Módulo 3: Agents con create_agent

Proyecto: Agente de Investigación con Tools

Descripción del proyecto

En las siete cápsulas anteriores aprendiste a crear agentes autónomos con create_agent, configurar system prompts estáticos y dinámicos, manejar el estado del agente con TypedDict, hacer streaming del proceso de razonamiento, obtener structured output con Pydantic models, y mapear APIs legacy a modernas. Cada concepto lo viste de forma individual. Ahora vas a combinar todo en un sistema real: un agente de investigación autónomo.

El agente recibe una pregunta de investigación — "¿Cuáles son las principales ventajas de Rust sobre C++?" o "¿Cómo funciona el sistema de tipos de TypeScript?" — y automáticamente busca información, extrae datos relevantes, hace cálculos si los necesita, y genera un reporte estructurado con hallazgos, fuentes, y nivel de confianza. Todo el proceso es visible mediante streaming: puedes ver en tiempo real cómo el agente razona, qué tools decide llamar, y cómo construye sus conclusiones.

Lo que hace a este proyecto interesante no es solo que el agente llama tools — eso ya lo hiciste en el Módulo 2. Lo diferente es que el agente opera de forma autónoma: tú le das la pregunta y él decide cuántas veces necesita buscar, qué datos extraer, si necesita calcular algo, y cuándo tiene suficiente información para generar el reporte final. El loop ReAct que aprendiste en la cápsula 02 maneja toda la orquestación.

El resultado es un sistema de investigación en terminal que demuestra todo lo aprendido en este módulo: create_agent para la orquestación, system prompt para guiar el comportamiento, streaming para visibilidad, y structured output para el reporte final.


Objetivo del proyecto

Construir un agente de investigación autónomo que recibe preguntas, busca información usando múltiples tools, y genera reportes estructurados — mostrando todo el proceso de razonamiento vía streaming.

Al completar este proyecto:

  • 🔧 Sabrás crear agentes autónomos con create_agent y múltiples tools
  • 🔧 Diseñarás system prompts que guían el comportamiento de investigación del agente
  • 🔧 Implementarás streaming para visualizar el proceso de razonamiento paso a paso
  • 🔧 Usarás structured output (Pydantic) para generar reportes con formato definido
  • 🔧 Tendrás un sistema de investigación funcional que integra todo el módulo

Especificaciones técnicas

Stack tecnológico

ComponenteVersiónPropósito
Python3.11+Runtime
LangChainv1.2+Framework de LLMs
LangGraphv1.0+create_agent
langchain-openailatestProveedor de modelo
python-dotenvlatestVariables de entorno
pydanticv2+Structured output

Setup inicial

pip install langchain langgraph langchain-openai python-dotenv pydantic

Crea un archivo .env en la raíz de tu proyecto:

# .env
OPENAI_API_KEY=sk-...

Estructura del proyecto

agente-investigacion/
├── .env                    # API key
├── research_agent.py       # Código principal (todo en un archivo)
└── requirements.txt        # Dependencias
# requirements.txt
langchain>=0.3.0
langgraph>=0.3.0
langchain-openai>=0.3.0
python-dotenv>=1.0.0
pydantic>=2.0.0

Paso 1: Crear las 3 herramientas de investigación

El agente necesita tres herramientas que cubren los pilares de una investigación: buscar información, extraer datos clave, y calcular métricas.

Tool 1: web_search (búsqueda web mock)

Simula una búsqueda web que retorna resultados sobre tecnología, ciencia, y temas generales. En un sistema real, esto conectaría con Tavily, DuckDuckGo, o Brave Search.

from langchain_core.tools import tool

KNOWLEDGE_BASE = {
    "python": {
        "title": "Python Programming Language",
        "content": (
            "Python es un lenguaje de programación de alto nivel, interpretado y de propósito general. "
            "Creado por Guido van Rossum en 1991. Usado en AI/ML, data science, web development, "
            "automatización y scripting. Tiene el ecosistema de librerías más grande para ML (PyTorch, "
            "TensorFlow, scikit-learn). Versión actual: 3.12. Tipado dinámico con type hints opcionales. "
            "Community: >8M developers activos."
        ),
        "source": "https://python.org",
    },
    "rust": {
        "title": "Rust Programming Language",
        "content": (
            "Rust es un lenguaje de programación de sistemas enfocado en seguridad, velocidad y "
            "concurrencia. Creado por Mozilla en 2010, v1.0 en 2015. Su sistema de ownership elimina "
            "null pointer errors y data races en compile time. Performance comparable a C/C++ sin "
            "garbage collector. Usado en sistemas operativos, browsers (Firefox, Chrome), CLI tools, "
            "y WebAssembly. Votado 'most loved language' 8 años seguidos en Stack Overflow."
        ),
        "source": "https://rust-lang.org",
    },
    "typescript": {
        "title": "TypeScript Language",
        "content": (
            "TypeScript es un superset tipado de JavaScript desarrollado por Microsoft. Agrega tipos "
            "estáticos opcionales, interfaces, enums, y generics. Compila a JavaScript plano. Adoptado "
            "por Angular, Vue 3, Deno, y la mayoría de frameworks modernos. El sistema de tipos es "
            "estructural (no nominal). 78% de developers JS profesionales usan TypeScript en 2025."
        ),
        "source": "https://typescriptlang.org",
    },
    "langchain": {
        "title": "LangChain Framework",
        "content": (
            "LangChain es el framework open-source más adoptado para construir aplicaciones con LLMs. "
            "Provee interfaces estandarizadas para modelos, tools, agentes, y workflows. Ecosistema: "
            "LangChain (alto nivel), LangGraph (orquestación), LangSmith (observability). API moderna "
            "v1.2+ con create_agent, middleware system, y Functional API. >80K stars en GitHub."
        ),
        "source": "https://python.langchain.com",
    },
    "kubernetes": {
        "title": "Kubernetes Container Orchestration",
        "content": (
            "Kubernetes (K8s) es un sistema de orquestación de contenedores open-source diseñado "
            "originalmente por Google. Automatiza despliegue, escalado, y gestión de aplicaciones "
            "containerizadas. Features: auto-scaling, rolling updates, service discovery, load "
            "balancing, self-healing. Cloud-native foundation (CNCF). Usado por >60% de empresas "
            "Fortune 500. Alternativas: Docker Swarm, ECS, Nomad."
        ),
        "source": "https://kubernetes.io",
    },
    "machine learning": {
        "title": "Machine Learning Overview",
        "content": (
            "Machine Learning es una rama de la inteligencia artificial que permite a los sistemas "
            "aprender de datos sin ser explícitamente programados. Tipos: supervised (clasificación, "
            "regresión), unsupervised (clustering, reducción de dimensionalidad), reinforcement "
            "learning. Frameworks principales: PyTorch, TensorFlow, scikit-learn, JAX. El mercado "
            "global de ML se estima en $209B para 2029 (CAGR 38.8%)."
        ),
        "source": "https://en.wikipedia.org/wiki/Machine_learning",
    },
}


@tool
def web_search(query: str) -> str:
    """Busca información en la web sobre cualquier tema.
    Retorna resultados con título, contenido y fuente.
    Útil para obtener datos, definiciones, comparaciones, y contexto general.
    """
    if not query or len(query.strip()) < 3:
        return "Error: la búsqueda necesita al menos 3 caracteres."

    query_lower = query.lower()
    results = []

    for keyword, data in KNOWLEDGE_BASE.items():
        if keyword in query_lower:
            results.append(
                f"📄 {data['title']}\n"
                f"   {data['content']}\n"
                f"   Fuente: {data['source']}"
            )

    if results:
        return f"Encontré {len(results)} resultado(s) para '{query}':\n\n" + "\n\n".join(results)

    return (
        f"Sin resultados específicos para '{query}'. "
        f"Temas disponibles: Python, Rust, TypeScript, LangChain, Kubernetes, Machine Learning. "
        f"Intenta reformular la búsqueda con alguno de estos temas."
    )

Tool 2: extract_info (extracción de datos mock)

Simula la extracción de datos clave de una fuente. En un sistema real, esto podría usar un scraper o un parser de documentos.

@tool
def extract_info(topic: str, aspect: str) -> str:
    """Extrae información específica sobre un aspecto de un tema.
    Parámetros:
    - topic: el tema principal (ej: 'Python', 'Rust')
    - aspect: qué aspecto extraer (ej: 'ventajas', 'desventajas', 'casos de uso', 'comparación')
    """
    if not topic or not aspect:
        return "Error: proporciona tanto el topic como el aspect."

    topic_lower = topic.lower().strip()
    aspect_lower = aspect.lower().strip()

    extractions = {
        "python": {
            "ventajas": (
                "Ventajas de Python:\n"
                "1. Sintaxis simple y legible — ideal para principiantes\n"
                "2. Ecosistema masivo — PyPI tiene >400K paquetes\n"
                "3. Comunidad enorme — >8M developers activos\n"
                "4. Versatilidad — web, AI/ML, scripting, automatización\n"
                "5. Productividad alta — prototipado rápido"
            ),
            "desventajas": (
                "Desventajas de Python:\n"
                "1. Velocidad de ejecución — 10-100x más lento que C/Rust\n"
                "2. GIL (Global Interpreter Lock) — limita concurrencia real\n"
                "3. Consumo de memoria — alto comparado con lenguajes compilados\n"
                "4. Mobile development — soporte limitado\n"
                "5. Runtime errors — tipado dinámico permite bugs en runtime"
            ),
            "casos de uso": (
                "Casos de uso principales de Python:\n"
                "1. Machine Learning / AI (PyTorch, TensorFlow)\n"
                "2. Data Science (pandas, numpy, matplotlib)\n"
                "3. Backend web (Django, FastAPI, Flask)\n"
                "4. Automatización y scripting\n"
                "5. DevOps y tooling"
            ),
        },
        "rust": {
            "ventajas": (
                "Ventajas de Rust:\n"
                "1. Memory safety sin garbage collector — ownership system\n"
                "2. Performance de C/C++ — zero-cost abstractions\n"
                "3. Concurrencia segura — data races imposibles en compile time\n"
                "4. Tooling excelente — cargo, clippy, rustfmt\n"
                "5. Interoperabilidad — FFI con C, WebAssembly nativo"
            ),
            "desventajas": (
                "Desventajas de Rust:\n"
                "1. Curva de aprendizaje pronunciada — borrow checker\n"
                "2. Tiempos de compilación largos\n"
                "3. Ecosistema más pequeño que Python/JS\n"
                "4. Menos developers disponibles en el mercado\n"
                "5. Verbosidad en código simple"
            ),
            "casos de uso": (
                "Casos de uso principales de Rust:\n"
                "1. Sistemas operativos y kernels\n"
                "2. Browsers y motores de renderizado\n"
                "3. CLI tools de alto performance\n"
                "4. WebAssembly\n"
                "5. Infraestructura cloud (Firecracker, TiKV)"
            ),
        },
        "typescript": {
            "ventajas": (
                "Ventajas de TypeScript:\n"
                "1. Tipos estáticos — errores detectados en compile time\n"
                "2. Mejor IDE support — autocompletado, refactoring\n"
                "3. Compatible con JavaScript existente\n"
                "4. Interfaces y generics — abstracción potente\n"
                "5. Adoptado por la industria — estándar de facto"
            ),
            "desventajas": (
                "Desventajas de TypeScript:\n"
                "1. Complejidad del sistema de tipos avanzado\n"
                "2. Paso de compilación adicional\n"
                "3. Tipos de librerías third-party pueden ser incorrectos\n"
                "4. Configuración inicial (tsconfig.json)\n"
                "5. Falsa sensación de seguridad — runtime sigue siendo JS"
            ),
            "casos de uso": (
                "Casos de uso principales de TypeScript:\n"
                "1. Frontend (React, Angular, Vue)\n"
                "2. Backend (Node.js, Deno, Bun)\n"
                "3. Full-stack frameworks (Next.js, Nuxt)\n"
                "4. CLI tools\n"
                "5. Librerías y SDKs"
            ),
        },
    }

    if topic_lower in extractions:
        topic_data = extractions[topic_lower]
        for key in topic_data:
            if key in aspect_lower:
                return topic_data[key]
        available = ", ".join(topic_data.keys())
        return f"No encontré el aspecto '{aspect}' para {topic}. Aspectos disponibles: {available}"

    available_topics = ", ".join(extractions.keys())
    return f"No tengo datos de extracción para '{topic}'. Temas disponibles: {available_topics}"

Tool 3: calculator (cálculos y métricas)

import math

@tool
def calculator(expression: str) -> str:
    """Evalúa expresiones matemáticas. Útil para calcular métricas,
    porcentajes, comparaciones numéricas, y estadísticas.
    Soporta: +, -, *, /, **, (), sqrt(), abs(), round().
    Ejemplos: '400000 / 8000000 * 100', 'sqrt(144)', '2**10'.
    """
    if not expression or not expression.strip():
        return "Error: expresión vacía."

    safe_dict = {
        "sqrt": math.sqrt,
        "abs": abs,
        "round": round,
        "pow": pow,
        "pi": math.pi,
        "e": math.e,
        "log": math.log,
        "log10": math.log10,
    }

    allowed_chars = set("0123456789+-*/.() ,epiabsqrtoundwlg")
    if not all(c in allowed_chars for c in expression.lower().replace(" ", "")):
        return f"Error: caracteres no permitidos en '{expression}'."

    try:
        result = eval(expression, {"__builtins__": {}}, safe_dict)
        if isinstance(result, float):
            if result == int(result) and abs(result) < 1e15:
                return str(int(result))
            return str(round(result, 4))
        return str(result)
    except ZeroDivisionError:
        return "Error: división por cero."
    except Exception as e:
        return f"Error al evaluar '{expression}': {e}"

Probemos las tres tools:

print(web_search.invoke({"query": "Python programming"}))
print()
print(extract_info.invoke({"topic": "Rust", "aspect": "ventajas"}))
print()
print(calculator.invoke({"expression": "400000 / 8000000 * 100"}))
# Output esperado:
# Encontré 1 resultado(s) para 'Python programming':
#
# 📄 Python Programming Language
#    Python es un lenguaje de programación...
#    Fuente: https://python.org
#
# Ventajas de Rust:
# 1. Memory safety sin garbage collector — ownership system
# ...
#
# 5

Paso 2: Crear el agente con create_agent y system prompt

El system prompt es crucial. Le dice al agente cómo comportarse como investigador: buscar información de forma exhaustiva, extraer datos específicos, y generar un reporte cuando tenga suficiente información.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langgraph.prebuilt import create_agent

RESEARCH_PROMPT = """Eres un agente de investigación especializado. Tu trabajo es investigar temas de forma exhaustiva y generar reportes informativos.

PROCESO DE INVESTIGACIÓN:
1. Primero, usa web_search para buscar información general sobre el tema
2. Luego, usa extract_info para obtener datos específicos (ventajas, desventajas, casos de uso)
3. Si necesitas calcular métricas o porcentajes, usa calculator
4. Cuando tengas suficiente información, genera tu reporte final

REGLAS:
- Siempre busca al menos 2 fuentes o aspectos diferentes antes de concluir
- Si una búsqueda no retorna resultados, reformula la query e intenta de nuevo
- Incluye datos concretos (números, porcentajes, años) cuando estén disponibles
- Cita las fuentes de donde obtuviste la información
- Si no encuentras información suficiente, dilo honestamente

Responde siempre en español."""

model = init_chat_model("openai:gpt-4.1-mini")

tools = [web_search, extract_info, calculator]

agent = create_agent(model, tools, prompt=RESEARCH_PROMPT)

Probemos con una pregunta simple:

result = agent.invoke(
    {"messages": [{"role": "user", "content": "Investiga sobre Rust"}]}
)
print(result["messages"][-1].content)
# Output esperado: Un reporte detallado sobre Rust con datos de web_search
# y extract_info, incluyendo ventajas, casos de uso, y estadísticas.

Paso 3: Agregar streaming para ver el proceso de razonamiento

El streaming permite ver en tiempo real cómo el agente piensa, qué tools decide llamar, y cómo construye su respuesta. Esto es fundamental para un agente de investigación — quieres ver el proceso, no solo el resultado.

def stream_research(question: str) -> str:
    """Ejecuta una investigación con streaming del proceso completo."""
    print(f"\n{'=' * 60}")
    print(f"  INVESTIGACIÓN: {question}")
    print(f"{'=' * 60}\n")

    final_content = ""

    for chunk in agent.stream(
        {"messages": [{"role": "user", "content": question}]},
        stream_mode="updates",
    ):
        for node_name, node_output in chunk.items():
            if node_name == "agent":
                messages = node_output.get("messages", [])
                for msg in messages:
                    if msg.content:
                        print(f"\n💭 Agente razonando:")
                        print(f"   {msg.content[:200]}")
                        final_content = msg.content

                    if hasattr(msg, "tool_calls") and msg.tool_calls:
                        for tc in msg.tool_calls:
                            args_preview = str(tc["args"])[:80]
                            print(f"\n🔧 Llamando tool: {tc['name']}")
                            print(f"   Args: {args_preview}")

            elif node_name == "tools":
                messages = node_output.get("messages", [])
                for msg in messages:
                    preview = msg.content[:100] + "..." if len(msg.content) > 100 else msg.content
                    print(f"   📥 Resultado: {preview}")

    print(f"\n{'=' * 60}")
    print(f"  INVESTIGACIÓN COMPLETADA")
    print(f"{'=' * 60}\n")

    return final_content

Probemos:

report = stream_research("¿Cuáles son las ventajas y desventajas de Python?")
print(f"\n📋 REPORTE FINAL:\n{report}")
# Output esperado:
# ============================================================
#   INVESTIGACIÓN: ¿Cuáles son las ventajas y desventajas de Python?
# ============================================================
#
# 🔧 Llamando tool: web_search
#    Args: {'query': 'Python programming ventajas desventajas'}
#    📥 Resultado: Encontré 1 resultado(s) para 'Python programming ventajas desventajas':...
#
# 🔧 Llamando tool: extract_info
#    Args: {'topic': 'Python', 'aspect': 'ventajas'}
#    📥 Resultado: Ventajas de Python: 1. Sintaxis simple y legible...
#
# 🔧 Llamando tool: extract_info
#    Args: {'topic': 'Python', 'aspect': 'desventajas'}
#    📥 Resultado: Desventajas de Python: 1. Velocidad de ejecución...
#
# 💭 Agente razonando:
#    [Reporte completo sobre Python]
#
# ============================================================
#   INVESTIGACIÓN COMPLETADA
# ============================================================

Paso 4: Agregar structured output para el reporte final

El agente genera reportes como texto libre. Para hacerlo profesional, definimos un modelo Pydantic que estructura el reporte con campos específicos: topic, summary, key findings, sources, y confidence score.

from pydantic import BaseModel, Field

class ResearchReport(BaseModel):
    topic: str = Field(description="El tema investigado")
    summary: str = Field(description="Resumen ejecutivo de 2-3 oraciones")
    key_findings: list[str] = Field(
        description="Lista de hallazgos clave (3-7 items)"
    )
    sources: list[str] = Field(
        description="URLs o referencias de las fuentes consultadas"
    )
    confidence: float = Field(
        description="Nivel de confianza en los resultados (0.0 a 1.0)",
        ge=0.0,
        le=1.0,
    )

Ahora creamos un segundo modelo con structured output para generar el reporte:

report_model = init_chat_model("openai:gpt-4.1-mini")
structured_report_model = report_model.with_structured_output(ResearchReport)


def generate_structured_report(raw_report: str, question: str) -> ResearchReport:
    """Convierte el reporte en texto libre a un ResearchReport estructurado."""
    prompt = (
        f"Basándote en esta investigación sobre '{question}', "
        f"genera un reporte estructurado.\n\n"
        f"Investigación:\n{raw_report}"
    )
    return structured_report_model.invoke(prompt)

Paso 5: Loop de investigación interactivo

Unimos todo en un loop interactivo que permite investigar múltiples temas.

def research_loop():
    """Loop interactivo de investigación."""
    print("=" * 60)
    print("  🔬 Agente de Investigación con Tools")
    print("  Escribe un tema para investigar")
    print("  Escribe 'salir' para terminar")
    print("=" * 60)

    while True:
        try:
            question = input("\n🔎 Pregunta de investigación: ").strip()
        except (KeyboardInterrupt, EOFError):
            print("\n\n¡Hasta luego!")
            break

        if not question:
            continue

        if question.lower() in ("salir", "exit", "quit"):
            print("\n¡Hasta luego!")
            break

        raw_report = stream_research(question)

        if not raw_report:
            print("❌ El agente no generó un reporte. Intenta con otra pregunta.")
            continue

        print("\n⏳ Generando reporte estructurado...")
        try:
            report = generate_structured_report(raw_report, question)

            print(f"\n{'=' * 60}")
            print(f"  📋 REPORTE ESTRUCTURADO")
            print(f"{'=' * 60}")
            print(f"\n📌 Tema: {report.topic}")
            print(f"\n📝 Resumen: {report.summary}")
            print(f"\n🔑 Hallazgos clave:")
            for i, finding in enumerate(report.key_findings, 1):
                print(f"   {i}. {finding}")
            print(f"\n📚 Fuentes:")
            for source in report.sources:
                print(f"   - {source}")
            print(f"\n📊 Confianza: {report.confidence:.0%}")
            print(f"{'=' * 60}")

        except Exception as e:
            print(f"\n⚠️ Error generando reporte estructurado: {e}")
            print(f"Reporte en texto libre:\n{raw_report}")

Código completo

Este es el archivo research_agent.py completo. Cópialo, configura tu .env, y ejecútalo con python research_agent.py:

"""
Agente de Investigación con Tools
Módulo 3 — LangChain & LangGraph: From Chains to Agents

Requiere: pip install langchain langgraph langchain-openai python-dotenv pydantic
"""

import math

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langgraph.prebuilt import create_agent
from pydantic import BaseModel, Field


# --- Knowledge Base (mock data) ---

KNOWLEDGE_BASE = {
    "python": {
        "title": "Python Programming Language",
        "content": (
            "Python es un lenguaje de programación de alto nivel, interpretado y de propósito general. "
            "Creado por Guido van Rossum en 1991. Usado en AI/ML, data science, web development, "
            "automatización y scripting. Tiene el ecosistema de librerías más grande para ML (PyTorch, "
            "TensorFlow, scikit-learn). Versión actual: 3.12. Tipado dinámico con type hints opcionales. "
            "Community: >8M developers activos."
        ),
        "source": "https://python.org",
    },
    "rust": {
        "title": "Rust Programming Language",
        "content": (
            "Rust es un lenguaje de programación de sistemas enfocado en seguridad, velocidad y "
            "concurrencia. Creado por Mozilla en 2010, v1.0 en 2015. Su sistema de ownership elimina "
            "null pointer errors y data races en compile time. Performance comparable a C/C++ sin "
            "garbage collector. Usado en sistemas operativos, browsers (Firefox, Chrome), CLI tools, "
            "y WebAssembly. Votado 'most loved language' 8 años seguidos en Stack Overflow."
        ),
        "source": "https://rust-lang.org",
    },
    "typescript": {
        "title": "TypeScript Language",
        "content": (
            "TypeScript es un superset tipado de JavaScript desarrollado por Microsoft. Agrega tipos "
            "estáticos opcionales, interfaces, enums, y generics. Compila a JavaScript plano. Adoptado "
            "por Angular, Vue 3, Deno, y la mayoría de frameworks modernos. El sistema de tipos es "
            "estructural (no nominal). 78% de developers JS profesionales usan TypeScript en 2025."
        ),
        "source": "https://typescriptlang.org",
    },
    "langchain": {
        "title": "LangChain Framework",
        "content": (
            "LangChain es el framework open-source más adoptado para construir aplicaciones con LLMs. "
            "Provee interfaces estandarizadas para modelos, tools, agentes, y workflows. Ecosistema: "
            "LangChain (alto nivel), LangGraph (orquestación), LangSmith (observability). API moderna "
            "v1.2+ con create_agent, middleware system, y Functional API. >80K stars en GitHub."
        ),
        "source": "https://python.langchain.com",
    },
    "kubernetes": {
        "title": "Kubernetes Container Orchestration",
        "content": (
            "Kubernetes (K8s) es un sistema de orquestación de contenedores open-source diseñado "
            "originalmente por Google. Automatiza despliegue, escalado, y gestión de aplicaciones "
            "containerizadas. Features: auto-scaling, rolling updates, service discovery, load "
            "balancing, self-healing. Cloud-native foundation (CNCF). Usado por >60% de empresas "
            "Fortune 500. Alternativas: Docker Swarm, ECS, Nomad."
        ),
        "source": "https://kubernetes.io",
    },
    "machine learning": {
        "title": "Machine Learning Overview",
        "content": (
            "Machine Learning es una rama de la inteligencia artificial que permite a los sistemas "
            "aprender de datos sin ser explícitamente programados. Tipos: supervised (clasificación, "
            "regresión), unsupervised (clustering, reducción de dimensionalidad), reinforcement "
            "learning. Frameworks principales: PyTorch, TensorFlow, scikit-learn, JAX. El mercado "
            "global de ML se estima en $209B para 2029 (CAGR 38.8%)."
        ),
        "source": "https://en.wikipedia.org/wiki/Machine_learning",
    },
}

EXTRACTIONS = {
    "python": {
        "ventajas": (
            "Ventajas de Python:\n"
            "1. Sintaxis simple y legible — ideal para principiantes\n"
            "2. Ecosistema masivo — PyPI tiene >400K paquetes\n"
            "3. Comunidad enorme — >8M developers activos\n"
            "4. Versatilidad — web, AI/ML, scripting, automatización\n"
            "5. Productividad alta — prototipado rápido"
        ),
        "desventajas": (
            "Desventajas de Python:\n"
            "1. Velocidad de ejecución — 10-100x más lento que C/Rust\n"
            "2. GIL (Global Interpreter Lock) — limita concurrencia real\n"
            "3. Consumo de memoria — alto comparado con lenguajes compilados\n"
            "4. Mobile development — soporte limitado\n"
            "5. Runtime errors — tipado dinámico permite bugs en runtime"
        ),
        "casos de uso": (
            "Casos de uso principales de Python:\n"
            "1. Machine Learning / AI (PyTorch, TensorFlow)\n"
            "2. Data Science (pandas, numpy, matplotlib)\n"
            "3. Backend web (Django, FastAPI, Flask)\n"
            "4. Automatización y scripting\n"
            "5. DevOps y tooling"
        ),
    },
    "rust": {
        "ventajas": (
            "Ventajas de Rust:\n"
            "1. Memory safety sin garbage collector — ownership system\n"
            "2. Performance de C/C++ — zero-cost abstractions\n"
            "3. Concurrencia segura — data races imposibles en compile time\n"
            "4. Tooling excelente — cargo, clippy, rustfmt\n"
            "5. Interoperabilidad — FFI con C, WebAssembly nativo"
        ),
        "desventajas": (
            "Desventajas de Rust:\n"
            "1. Curva de aprendizaje pronunciada — borrow checker\n"
            "2. Tiempos de compilación largos\n"
            "3. Ecosistema más pequeño que Python/JS\n"
            "4. Menos developers disponibles en el mercado\n"
            "5. Verbosidad en código simple"
        ),
        "casos de uso": (
            "Casos de uso principales de Rust:\n"
            "1. Sistemas operativos y kernels\n"
            "2. Browsers y motores de renderizado\n"
            "3. CLI tools de alto performance\n"
            "4. WebAssembly\n"
            "5. Infraestructura cloud (Firecracker, TiKV)"
        ),
    },
    "typescript": {
        "ventajas": (
            "Ventajas de TypeScript:\n"
            "1. Tipos estáticos — errores detectados en compile time\n"
            "2. Mejor IDE support — autocompletado, refactoring\n"
            "3. Compatible con JavaScript existente\n"
            "4. Interfaces y generics — abstracción potente\n"
            "5. Adoptado por la industria — estándar de facto"
        ),
        "desventajas": (
            "Desventajas de TypeScript:\n"
            "1. Complejidad del sistema de tipos avanzado\n"
            "2. Paso de compilación adicional\n"
            "3. Tipos de librerías third-party pueden ser incorrectos\n"
            "4. Configuración inicial (tsconfig.json)\n"
            "5. Falsa sensación de seguridad — runtime sigue siendo JS"
        ),
        "casos de uso": (
            "Casos de uso principales de TypeScript:\n"
            "1. Frontend (React, Angular, Vue)\n"
            "2. Backend (Node.js, Deno, Bun)\n"
            "3. Full-stack frameworks (Next.js, Nuxt)\n"
            "4. CLI tools\n"
            "5. Librerías y SDKs"
        ),
    },
}


# --- Tools ---

@tool
def web_search(query: str) -> str:
    """Busca información en la web sobre cualquier tema.
    Retorna resultados con título, contenido y fuente.
    Útil para obtener datos, definiciones, comparaciones, y contexto general.
    """
    if not query or len(query.strip()) < 3:
        return "Error: la búsqueda necesita al menos 3 caracteres."

    query_lower = query.lower()
    results = []

    for keyword, data in KNOWLEDGE_BASE.items():
        if keyword in query_lower:
            results.append(
                f"📄 {data['title']}\n"
                f"   {data['content']}\n"
                f"   Fuente: {data['source']}"
            )

    if results:
        return f"Encontré {len(results)} resultado(s) para '{query}':\n\n" + "\n\n".join(results)

    return (
        f"Sin resultados específicos para '{query}'. "
        f"Temas disponibles: Python, Rust, TypeScript, LangChain, Kubernetes, Machine Learning. "
        f"Intenta reformular la búsqueda con alguno de estos temas."
    )


@tool
def extract_info(topic: str, aspect: str) -> str:
    """Extrae información específica sobre un aspecto de un tema.
    Parámetros:
    - topic: el tema principal (ej: 'Python', 'Rust')
    - aspect: qué aspecto extraer (ej: 'ventajas', 'desventajas', 'casos de uso')
    """
    if not topic or not aspect:
        return "Error: proporciona tanto el topic como el aspect."

    topic_lower = topic.lower().strip()
    aspect_lower = aspect.lower().strip()

    if topic_lower in EXTRACTIONS:
        topic_data = EXTRACTIONS[topic_lower]
        for key in topic_data:
            if key in aspect_lower:
                return topic_data[key]
        available = ", ".join(topic_data.keys())
        return f"No encontré el aspecto '{aspect}' para {topic}. Aspectos disponibles: {available}"

    available_topics = ", ".join(EXTRACTIONS.keys())
    return f"No tengo datos de extracción para '{topic}'. Temas disponibles: {available_topics}"


@tool
def calculator(expression: str) -> str:
    """Evalúa expresiones matemáticas. Útil para calcular métricas,
    porcentajes, comparaciones numéricas, y estadísticas.
    Soporta: +, -, *, /, **, (), sqrt(), abs(), round().
    Ejemplos: '400000 / 8000000 * 100', 'sqrt(144)', '2**10'.
    """
    if not expression or not expression.strip():
        return "Error: expresión vacía."

    safe_dict = {
        "sqrt": math.sqrt,
        "abs": abs,
        "round": round,
        "pow": pow,
        "pi": math.pi,
        "e": math.e,
        "log": math.log,
        "log10": math.log10,
    }

    allowed_chars = set("0123456789+-*/.() ,epiabsqrtoundwlg")
    if not all(c in allowed_chars for c in expression.lower().replace(" ", "")):
        return f"Error: caracteres no permitidos en '{expression}'."

    try:
        result = eval(expression, {"__builtins__": {}}, safe_dict)
        if isinstance(result, float):
            if result == int(result) and abs(result) < 1e15:
                return str(int(result))
            return str(round(result, 4))
        return str(result)
    except ZeroDivisionError:
        return "Error: división por cero."
    except Exception as e:
        return f"Error al evaluar '{expression}': {e}"


# --- Structured Output Model ---

class ResearchReport(BaseModel):
    topic: str = Field(description="El tema investigado")
    summary: str = Field(description="Resumen ejecutivo de 2-3 oraciones")
    key_findings: list[str] = Field(
        description="Lista de hallazgos clave (3-7 items)"
    )
    sources: list[str] = Field(
        description="URLs o referencias de las fuentes consultadas"
    )
    confidence: float = Field(
        description="Nivel de confianza en los resultados (0.0 a 1.0)",
        ge=0.0,
        le=1.0,
    )


# --- Agent Setup ---

RESEARCH_PROMPT = """Eres un agente de investigación especializado. Tu trabajo es investigar temas de forma exhaustiva y generar reportes informativos.

PROCESO DE INVESTIGACIÓN:
1. Primero, usa web_search para buscar información general sobre el tema
2. Luego, usa extract_info para obtener datos específicos (ventajas, desventajas, casos de uso)
3. Si necesitas calcular métricas o porcentajes, usa calculator
4. Cuando tengas suficiente información, genera tu reporte final

REGLAS:
- Siempre busca al menos 2 fuentes o aspectos diferentes antes de concluir
- Si una búsqueda no retorna resultados, reformula la query e intenta de nuevo
- Incluye datos concretos (números, porcentajes, años) cuando estén disponibles
- Cita las fuentes de donde obtuviste la información
- Si no encuentras información suficiente, dilo honestamente

Responde siempre en español."""

model = init_chat_model("openai:gpt-4.1-mini")

tools = [web_search, extract_info, calculator]

agent = create_agent(model, tools, prompt=RESEARCH_PROMPT)

report_model = init_chat_model("openai:gpt-4.1-mini")
structured_report_model = report_model.with_structured_output(ResearchReport)


# --- Streaming ---

def stream_research(question: str) -> str:
    """Ejecuta una investigación con streaming del proceso completo."""
    print(f"\n{'=' * 60}")
    print(f"  INVESTIGACIÓN: {question}")
    print(f"{'=' * 60}\n")

    final_content = ""

    for chunk in agent.stream(
        {"messages": [{"role": "user", "content": question}]},
        stream_mode="updates",
    ):
        for node_name, node_output in chunk.items():
            if node_name == "agent":
                messages = node_output.get("messages", [])
                for msg in messages:
                    if msg.content:
                        print(f"\n💭 Agente razonando:")
                        print(f"   {msg.content[:200]}")
                        final_content = msg.content

                    if hasattr(msg, "tool_calls") and msg.tool_calls:
                        for tc in msg.tool_calls:
                            args_preview = str(tc["args"])[:80]
                            print(f"\n🔧 Llamando tool: {tc['name']}")
                            print(f"   Args: {args_preview}")

            elif node_name == "tools":
                messages = node_output.get("messages", [])
                for msg in messages:
                    preview = msg.content[:100] + "..." if len(msg.content) > 100 else msg.content
                    print(f"   📥 Resultado: {preview}")

    print(f"\n{'=' * 60}")
    print(f"  INVESTIGACIÓN COMPLETADA")
    print(f"{'=' * 60}\n")

    return final_content


def generate_structured_report(raw_report: str, question: str) -> ResearchReport:
    """Convierte el reporte en texto libre a un ResearchReport estructurado."""
    prompt = (
        f"Basándote en esta investigación sobre '{question}', "
        f"genera un reporte estructurado.\n\n"
        f"Investigación:\n{raw_report}"
    )
    return structured_report_model.invoke(prompt)


# --- Interactive Loop ---

def research_loop():
    """Loop interactivo de investigación."""
    print("=" * 60)
    print("  🔬 Agente de Investigación con Tools")
    print("  Escribe un tema para investigar")
    print("  Escribe 'salir' para terminar")
    print("=" * 60)

    while True:
        try:
            question = input("\n🔎 Pregunta de investigación: ").strip()
        except (KeyboardInterrupt, EOFError):
            print("\n\n¡Hasta luego!")
            break

        if not question:
            continue

        if question.lower() in ("salir", "exit", "quit"):
            print("\n¡Hasta luego!")
            break

        raw_report = stream_research(question)

        if not raw_report:
            print("❌ El agente no generó un reporte. Intenta con otra pregunta.")
            continue

        print("\n⏳ Generando reporte estructurado...")
        try:
            report = generate_structured_report(raw_report, question)

            print(f"\n{'=' * 60}")
            print(f"  📋 REPORTE ESTRUCTURADO")
            print(f"{'=' * 60}")
            print(f"\n📌 Tema: {report.topic}")
            print(f"\n📝 Resumen: {report.summary}")
            print(f"\n🔑 Hallazgos clave:")
            for i, finding in enumerate(report.key_findings, 1):
                print(f"   {i}. {finding}")
            print(f"\n📚 Fuentes:")
            for source in report.sources:
                print(f"   - {source}")
            print(f"\n📊 Confianza: {report.confidence:.0%}")
            print(f"{'=' * 60}")

        except Exception as e:
            print(f"\n⚠️ Error generando reporte estructurado: {e}")
            print(f"Reporte en texto libre:\n{raw_report}")


if __name__ == "__main__":
    research_loop()

Ejecútalo:

python research_agent.py

Criterios de éxito

Tu proyecto está completo cuando cumples los cuatro criterios:

  • Agente ejecuta múltiples tool calls en secuencia autónoma — el agente decide por sí mismo cuándo buscar, cuándo extraer, y cuándo calcular. No le dices qué tools usar; él lo decide
  • Streaming muestra el proceso de pensamiento — puedes ver en tiempo real qué tools llama el agente, con qué argumentos, y qué resultados obtiene
  • Reporte final tiene estructura definida (Pydantic model) — el ResearchReport contiene topic, summary, key_findings, sources, y confidence como campos tipados
  • Agente se detiene después de recopilar suficiente información — no entra en loop infinito; cuando tiene suficiente data, genera el reporte y termina

Cómo probar con diferentes temas

Test 1: Investigación de un lenguaje

🔎 Pregunta de investigación: ¿Cuáles son las ventajas de Rust sobre otros lenguajes?

🔧 Llamando tool: web_search
   Args: {'query': 'Rust programming language ventajas'}
   📥 Resultado: Encontré 1 resultado(s) para 'Rust programming language ventajas': 📄 Rust Progr...

🔧 Llamando tool: extract_info
   Args: {'topic': 'Rust', 'aspect': 'ventajas'}
   📥 Resultado: Ventajas de Rust: 1. Memory safety sin garbage collector...

🔧 Llamando tool: extract_info
   Args: {'topic': 'Rust', 'aspect': 'casos de uso'}
   📥 Resultado: Casos de uso principales de Rust: 1. Sistemas operativos y kernels...

💭 Agente razonando:
   [Reporte detallado sobre Rust]

📋 REPORTE ESTRUCTURADO
📌 Tema: Rust Programming Language
📝 Resumen: Rust es un lenguaje de sistemas que combina...
🔑 Hallazgos clave:
   1. Memory safety sin garbage collector...
   2. Performance comparable a C/C++...
   3. ...
📚 Fuentes:
   - https://rust-lang.org
📊 Confianza: 85%

Test 2: Investigación con cálculos

🔎 Pregunta de investigación: Compara la adopción de Python vs TypeScript en la industria

🔧 Llamando tool: web_search
   Args: {'query': 'Python programming adoption'}
   📥 Resultado: ...

🔧 Llamando tool: web_search
   Args: {'query': 'TypeScript adoption'}
   📥 Resultado: ...

🔧 Llamando tool: calculator
   Args: {'expression': '8000000 / 78 * 100'}
   📥 Resultado: ...

💭 Agente razonando:
   [Comparación detallada con números]

Test 3: Tema sin datos disponibles

🔎 Pregunta de investigación: ¿Cómo funciona la fotosíntesis?

🔧 Llamando tool: web_search
   Args: {'query': 'fotosíntesis'}
   📥 Resultado: Sin resultados específicos para 'fotosíntesis'...

💭 Agente razonando:
   No encontré información suficiente sobre fotosíntesis en mis fuentes.
   Los temas disponibles son tecnología...

Test 4: Investigación que requiere múltiples aspectos

🔎 Pregunta de investigación: Haz un análisis completo de Python: ventajas, desventajas y casos de uso

🔧 Llamando tool: web_search
   Args: {'query': 'Python programming language'}
🔧 Llamando tool: extract_info
   Args: {'topic': 'Python', 'aspect': 'ventajas'}
🔧 Llamando tool: extract_info
   Args: {'topic': 'Python', 'aspect': 'desventajas'}
🔧 Llamando tool: extract_info
   Args: {'topic': 'Python', 'aspect': 'casos de uso'}

💭 Agente razonando:
   [Análisis completo con 4 fuentes de datos]

Errores comunes

1. ModuleNotFoundError: No module named 'langgraph'

Causa: No instalaste langgraph, que es un paquete separado de langchain.

pip install langgraph

2. ImportError: cannot import name 'create_agent' from 'langgraph.prebuilt'

Causa: Versión antigua de langgraph. create_agent requiere langgraph>=0.3.0.

pip install --upgrade langgraph

3. El agente no llama tools y responde directamente

Causa: El system prompt no es lo suficientemente directivo, o la pregunta es demasiado general. El modelo decide que puede responder sin tools.

Solución: Haz el system prompt más explícito sobre cuándo usar tools. La línea "Siempre busca al menos 2 fuentes o aspectos diferentes antes de concluir" ayuda, pero puedes reforzar con "SIEMPRE usa web_search como primer paso".

4. ValidationError en el ResearchReport

pydantic.ValidationError: 1 validation error for ResearchReport
confidence: Input should be less than or equal to 1

Causa: El modelo generó un confidence mayor a 1.0 (por ejemplo, 85 en vez de 0.85).

Solución: El Field ya tiene ge=0.0, le=1.0. Si el error persiste, agrega una instrucción en el prompt de generate_structured_report: "confidence debe ser un float entre 0.0 y 1.0 (no un porcentaje)".

5. Streaming no muestra nada y luego imprime todo de golpe

Causa: Estás usando stream_mode="values" que espera a que cada nodo termine. Usa stream_mode="updates" para ver actualizaciones incrementales.

Solución: Verifica que el código usa stream_mode="updates":

for chunk in agent.stream(
    {"messages": [...]},
    stream_mode="updates",  # ← no "values"
):

6. El agente entra en loop infinito de tool calls

Causa: El agente no encuentra información suficiente y sigue intentando con reformulaciones de la query. Esto es más común con temas que no están en la knowledge base.

Solución: create_agent tiene un límite de iteraciones por defecto. Si necesitas ajustarlo, pasa max_iterations al crear el agente:

agent = create_agent(model, tools, prompt=RESEARCH_PROMPT)
result = agent.invoke(
    {"messages": [...]},
    config={"recursion_limit": 25},
)

7. with_structured_output retorna None

Causa: El modelo no pudo generar output que cumpla con el schema. Esto puede pasar si el reporte de input es muy corto o no contiene suficiente información para llenar todos los campos.

Solución: Agrega un try/except y usa el reporte raw como fallback:

try:
    report = generate_structured_report(raw_report, question)
except Exception:
    print("Usando reporte en texto libre (structured output falló)")
    print(raw_report)

8. AuthenticationError o RateLimitError

Causa: API key inválida o excediste tu límite de uso. El agente hace múltiples llamadas al modelo (una por cada iteración del loop ReAct + una para structured output).

Solución: Verifica tu API key y tu usage. El agente puede hacer 5-10 llamadas al modelo por investigación, así que cada investigación consume más tokens que una sola llamada.


Ideas para extender

Si terminaste el proyecto y quieres ir más allá:

  • 🚀 APIs reales — Reemplaza web_search mock con Tavily Search API (tavily-python) o DuckDuckGo Search (duckduckgo-search). Reemplaza extract_info con un scraper real usando beautifulsoup4
  • 🚀 Memoria entre investigaciones — Agrega checkpointer=MemorySaver() al agente para que recuerde investigaciones previas. Puedes preguntar "¿Qué investigaste antes sobre Python?" y el agente lo recuerda
  • 🚀 Exportar reportes — Agrega una tool que escriba el ResearchReport como archivo Markdown o JSON. Usa json.dumps(report.model_dump(), indent=2) para serializar
  • 🚀 Múltiples modelos — Usa un modelo rápido (gpt-4.1-mini) para las iteraciones de tool calling y un modelo potente (gpt-4.1) para generar el structured report final
  • 🚀 Validación del reporte — Agrega un paso donde otro modelo revisa el reporte y sugiere correcciones o áreas que faltan investigar
  • 🚀 Dashboard de métricas — Trackea cuántas tools se llamaron, cuántos tokens se consumieron, y cuánto tardó cada investigación

Conexión con el siguiente módulo

En este proyecto creaste un agente funcional con create_agent, pero toda la customización se limitó al system prompt y las tools. ¿Qué pasa si quieres que el agente use un modelo económico para la búsqueda y un modelo potente para generar el reporte? ¿O que filtre tools dependiendo del tipo de pregunta? ¿O que logguee cada llamada al modelo para monitoring?

En el Módulo 4: Middleware y Customización, aprenderás a interceptar y modificar el comportamiento del agente sin reescribirlo. Con @wrap_model_call puedes cambiar el modelo en runtime, con @wrap_tool_call puedes agregar logging a cada tool call, y con @dynamic_prompt puedes cambiar el system prompt basado en el contexto. El mismo agente de investigación que construiste aquí podría beneficiarse enormemente de middleware: routing dinámico de modelos (mini para search, potente para reporte), logging de cada tool call, y prompts que se adaptan al tema de investigación.


Recursos para el proyecto

  1. LangGraph create_agent — Referencia de la API de create_agent
  2. Streaming en LangGraph — Guía oficial de streaming de agentes
  3. Structured Output — Guía de with_structured_output con Pydantic
  4. LangChain Tools — Conceptos de tools en LangChain
  5. ReAct Pattern Paper — Paper original de ReAct (Reason + Act)
  6. Pydantic v2 Documentation — Referencia de Pydantic para structured models

Módulo 3 — LangChain & LangGraph: From Chains to Agents