Módulo 4: Middleware y Customización

Dynamic Tools y Dynamic Prompts

Descripción de la cápsula

En la cápsula anterior aprendiste a cambiar el modelo dinámicamente según la complejidad. Pero hay dos dimensiones más que puedes personalizar en tiempo real: las tools disponibles y el system prompt del agente.

No todos los usuarios deben tener acceso a todas las tools. Un usuario con rol admin puede eliminar registros; un usuario basic no. Un usuario del tier gratuito tiene 3 tools; el del tier premium tiene 10. Si expones todas las tools a todos los usuarios, estás creando un problema de seguridad y confundiendo al modelo con opciones irrelevantes.

Lo mismo aplica al system prompt. Un prompt estático funciona para prototipos, pero en producción quieres que el prompt se adapte: incluir el nombre del usuario, describir solo las tools disponibles, ajustar el tono según el contexto, o agregar instrucciones específicas según el estado de la conversación.

En LangChain v1.2+, create_agent acepta una función en lugar de una lista para tools, y un callable para prompt. Esto te da control total sobre qué tools y qué prompt ve el agente en cada invocación — sin reescribir el agente.


Dynamic tools: el concepto

Normalmente, pasas una lista estática de tools al agente:

# Estático: siempre las mismas tools
agent = create_agent(model, tools=[search, calculator, delete_record])

Con dynamic tools, pasas una función que recibe el estado y retorna la lista de tools apropiada:

# Dinámico: las tools cambian según el estado
agent = create_agent(model, tools=get_tools_for_user)

¿Por qué importa?

Cuando un LLM recibe herramientas en su contexto, las considera opciones válidas. Si le das delete_record a un usuario sin permisos, el modelo puede intentar usarla — y tu sistema tiene que manejar el rechazo. Es mejor no darle la opción: si el usuario no puede borrar registros, el modelo no debería saber que esa tool existe.

Enfoque estático:
  Modelo recibe: [search, calculator, delete_record]
  → Puede intentar delete_record → Error de permisos → Mala UX

Enfoque dinámico:
  Modelo recibe: [search, calculator]  (sin delete_record)
  → Ni siquiera sabe que existe → Nunca lo intenta → Limpio

Implementando dynamic tools con create_agent

La forma más directa: pasar una función al parámetro tools de create_agent. Esta función recibe el estado actual y retorna la lista de tools.

Filtrado por rol de usuario

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es un concepto relevante."

@tool
def calculator(expression: str) -> str:
    """Evalúa una expresión matemática."""
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

@tool
def delete_record(record_id: str) -> str:
    """Elimina un registro de la base de datos. Solo administradores."""
    return f"Registro {record_id} eliminado exitosamente."

@tool
def modify_config(key: str, value: str) -> str:
    """Modifica la configuración del sistema. Solo administradores."""
    return f"Configuración {key}={value} actualizada."

def get_tools_for_user(state) -> list:
    """Retorna tools basados en el rol del usuario."""
    user_role = state.get("user_role", "basic")

    base_tools = [search, calculator]
    admin_tools = [search, calculator, delete_record, modify_config]

    if user_role == "admin":
        print(f"[TOOLS] Rol: admin → {len(admin_tools)} tools disponibles")
        return admin_tools

    print(f"[TOOLS] Rol: {user_role}{len(base_tools)} tools disponibles")
    return base_tools

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

agent = create_agent(
    model,
    tools=get_tools_for_user,
)

result_basic = agent.invoke({
    "messages": [("user", "Busca información sobre Python")],
    "user_role": "basic",
})
print(f"[basic] Respuesta: {result_basic['messages'][-1].content[:80]}")

print()

result_admin = agent.invoke({
    "messages": [("user", "Elimina el registro con id=42")],
    "user_role": "admin",
})
print(f"[admin] Respuesta: {result_admin['messages'][-1].content[:80]}")
# Output esperado:
# [TOOLS] Rol: basic → 2 tools disponibles
# [basic] Respuesta: Python es un lenguaje de programación de alto nivel, interpretado y de propósito...
# 
# [TOOLS] Rol: admin → 4 tools disponibles
# [admin] Respuesta: El registro con id=42 ha sido eliminado exitosamente.

El agente basic ni siquiera ve delete_record — no puede intentar usarla porque no existe en su contexto.


Casos de uso: filtrado de tools por contexto

Free vs Paid tier

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search_web(query: str) -> str:
    """Busca información en la web."""
    return f"Resultado web: {query} encontrado."

@tool
def search_academic(query: str) -> str:
    """Busca en bases de datos académicas (requiere plan premium)."""
    return f"Paper encontrado: '{query}' - Journal of AI Research, 2025."

@tool
def summarize(text: str) -> str:
    """Resume un texto largo."""
    return f"Resumen: {text[:50]}..."

@tool
def translate(text: str, target_lang: str) -> str:
    """Traduce texto a otro idioma (requiere plan premium)."""
    return f"[{target_lang}] {text}"

@tool
def export_pdf(content: str) -> str:
    """Exporta contenido a PDF (requiere plan premium)."""
    return f"PDF generado con {len(content)} caracteres."

TIER_TOOLS = {
    "free": [search_web, summarize],
    "premium": [search_web, search_academic, summarize, translate, export_pdf],
}

def get_tools_by_tier(state) -> list:
    """Retorna tools según el tier del usuario."""
    tier = state.get("user_tier", "free")
    tools = TIER_TOOLS.get(tier, TIER_TOOLS["free"])
    tool_names = [t.name for t in tools]
    print(f"[TIER] {tier} → tools: {tool_names}")
    return tools

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_tools_by_tier)

result_free = agent.invoke({
    "messages": [("user", "Busca información sobre machine learning")],
    "user_tier": "free",
})
print(f"[free] {result_free['messages'][-1].content[:80]}\n")

result_premium = agent.invoke({
    "messages": [("user", "Busca papers académicos sobre transformers y tradúcelos al español")],
    "user_tier": "premium",
})
print(f"[premium] {result_premium['messages'][-1].content[:80]}")
# Output esperado:
# [TIER] free → tools: ['search_web', 'summarize']
# [free] Machine learning es un campo de la inteligencia artificial que permite a los sistem...
# 
# [TIER] premium → tools: ['search_web', 'search_academic', 'summarize', 'translate', 'export_pdf']
# [premium] Encontré un paper relevante: 'transformers' del Journal of AI Research, 2025. A...

Producción vs Desarrollo

from dotenv import load_dotenv
load_dotenv()

import os
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información."""
    return f"Resultado: {query}."

@tool
def query_database(sql: str) -> str:
    """Ejecuta una consulta SQL en la base de datos."""
    return f"Query ejecutada: {sql} → 42 filas retornadas."

@tool
def debug_inspect(variable: str) -> str:
    """Inspecciona una variable del sistema (solo desarrollo)."""
    return f"DEBUG: {variable} = {{type: 'dict', keys: ['a', 'b'], memory: '2.3MB'}}"

@tool
def reset_database(confirm: str) -> str:
    """Resetea la base de datos a su estado inicial (solo desarrollo)."""
    return f"Database reseteada. Confirm={confirm}"

def get_tools_by_environment(state) -> list:
    """Retorna tools según el entorno."""
    env = state.get("environment", os.getenv("APP_ENV", "production"))

    prod_tools = [search, query_database]
    dev_tools = [search, query_database, debug_inspect, reset_database]

    if env == "development":
        print(f"[ENV] development → {len(dev_tools)} tools (incluye debug)")
        return dev_tools

    print(f"[ENV] production → {len(prod_tools)} tools (sin debug)")
    return prod_tools

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_tools_by_environment)

result = agent.invoke({
    "messages": [("user", "Busca información sobre Docker")],
    "environment": "production",
})
print(f"[prod] {result['messages'][-1].content[:80]}")
# Output esperado:
# [ENV] production → 2 tools (sin debug)
# [prod] Docker es una plataforma de contenedorización que permite empaquetar aplicaciones...

Registrando tools en runtime

A veces las tools no están predefinidas. Pueden venir de servidores MCP, plugins, o APIs descubiertas en runtime. Puedes construir tools dinámicamente y agregarlas al agente.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool, StructuredTool
from pydantic import BaseModel, Field

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es un concepto relevante."

class APIEndpointInput(BaseModel):
    endpoint: str = Field(description="El endpoint de la API a llamar")
    method: str = Field(default="GET", description="Método HTTP")

def create_api_tool(api_name: str, base_url: str) -> StructuredTool:
    """Crea una tool dinámica para una API específica."""

    def call_api(endpoint: str, method: str = "GET") -> str:
        url = f"{base_url}{endpoint}"
        return f"[{api_name}] {method} {url} → 200 OK (datos simulados)"

    return StructuredTool.from_function(
        func=call_api,
        name=f"call_{api_name.lower().replace(' ', '_')}",
        description=f"Llama a la API de {api_name}. Base URL: {base_url}",
        args_schema=APIEndpointInput,
    )

AVAILABLE_APIS = {
    "weather": {"name": "Weather Service", "url": "https://api.weather.example.com"},
    "news": {"name": "News Service", "url": "https://api.news.example.com"},
    "stocks": {"name": "Stock Market", "url": "https://api.stocks.example.com"},
}

def get_tools_with_apis(state) -> list:
    """Retorna tools base + tools dinámicas de APIs conectadas."""
    base_tools = [search]

    connected_apis = state.get("connected_apis", [])
    for api_key in connected_apis:
        if api_key in AVAILABLE_APIS:
            api_info = AVAILABLE_APIS[api_key]
            api_tool = create_api_tool(api_info["name"], api_info["url"])
            base_tools.append(api_tool)

    tool_names = [t.name for t in base_tools]
    print(f"[RUNTIME] APIs conectadas: {connected_apis} → tools: {tool_names}")
    return base_tools

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_tools_with_apis)

result = agent.invoke({
    "messages": [("user", "¿Cuál es el clima actual y las noticias del día?")],
    "connected_apis": ["weather", "news"],
})
print(f"\nRespuesta: {result['messages'][-1].content[:120]}")
# Output esperado:
# [RUNTIME] APIs conectadas: ['weather', 'news'] → tools: ['search', 'call_weather_service', 'call_news_service']
# 
# Respuesta: Según la información obtenida, el clima actual muestra condiciones favorables y las noticias del día incluyen temas rel...

Las tools se crean en el momento de la invocación, basándose en qué APIs tiene conectadas el usuario. Si mañana agregas una nueva API, no necesitas modificar el agente.


Dynamic prompts: más allá de strings estáticos

Un system prompt estático funciona para casos simples:

# Estático
agent = create_agent(model, tools, prompt="Eres un asistente útil.")

Pero en producción necesitas que el prompt se adapte. create_agent acepta un callable que recibe el estado y retorna el prompt:

# Dinámico
agent = create_agent(model, tools, prompt=generate_prompt)

Prompt dinámico basado en el usuario

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es un concepto clave en tecnología."

@tool
def calculator(expression: str) -> str:
    """Evalúa una expresión matemática."""
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

def generate_prompt(state) -> str:
    """Genera el system prompt basado en el estado actual."""
    user_name = state.get("user_name", "usuario")
    user_role = state.get("user_role", "basic")
    language = state.get("language", "es")

    role_instructions = {
        "admin": "Tienes acceso completo al sistema. Puedes ejecutar operaciones destructivas si el usuario lo confirma.",
        "basic": "Tienes acceso limitado. No puedes eliminar datos ni modificar configuraciones.",
    }

    lang_instructions = {
        "es": "Responde siempre en español.",
        "en": "Always respond in English.",
    }

    prompt = f"""Eres un asistente inteligente. El usuario se llama {user_name}.

{role_instructions.get(user_role, role_instructions['basic'])}
{lang_instructions.get(language, lang_instructions['es'])}

Sé conciso y directo en tus respuestas."""

    print(f"[PROMPT] Generado para: {user_name} (rol={user_role}, lang={language})")
    return prompt

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

agent = create_agent(
    model,
    tools=[search, calculator],
    prompt=generate_prompt,
)

result = agent.invoke({
    "messages": [("user", "¿Quién soy y qué puedo hacer?")],
    "user_name": "Carlos",
    "user_role": "admin",
    "language": "es",
})
print(f"\nRespuesta: {result['messages'][-1].content[:150]}")
# Output esperado:
# [PROMPT] Generado para: Carlos (rol=admin, lang=es)
# 
# Respuesta: ¡Hola Carlos! Eres un administrador con acceso completo al sistema. Puedes buscar información, realizar cálculos, eliminar registros y modificar confi...

Prompt que lista las tools disponibles

Un patrón poderoso: el prompt describe las tools que el agente tiene. Si las tools cambian dinámicamente, el prompt debe reflejar eso.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información general sobre un tema."""
    return f"Resultado: {query} es un concepto importante."

@tool
def calculator(expression: str) -> str:
    """Evalúa expresiones matemáticas."""
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

@tool
def delete_record(record_id: str) -> str:
    """Elimina un registro de la base de datos."""
    return f"Registro {record_id} eliminado."

ALL_TOOLS = {
    "search": search,
    "calculator": calculator,
    "delete_record": delete_record,
}

TOOL_DESCRIPTIONS = {
    "search": "buscar información general",
    "calculator": "hacer cálculos matemáticos",
    "delete_record": "eliminar registros de la base de datos",
}

def get_tools_for_state(state) -> list:
    user_role = state.get("user_role", "basic")
    if user_role == "admin":
        return list(ALL_TOOLS.values())
    return [ALL_TOOLS["search"], ALL_TOOLS["calculator"]]

def generate_prompt_with_tools(state) -> str:
    """Genera prompt que describe las tools disponibles."""
    user_role = state.get("user_role", "basic")
    user_name = state.get("user_name", "usuario")

    available_tool_names = [t.name for t in get_tools_for_state(state)]
    tool_list = "\n".join(
        f"- {name}: {TOOL_DESCRIPTIONS[name]}"
        for name in available_tool_names
    )

    prompt = f"""Eres un asistente para {user_name} (rol: {user_role}).

Tienes acceso a las siguientes herramientas:
{tool_list}

Usa solo las herramientas listadas arriba. Si el usuario pide algo que requiere una herramienta que no tienes, explica amablemente que no tienes acceso a esa funcionalidad."""

    print(f"[PROMPT] Tools en prompt: {available_tool_names}")
    return prompt

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

agent = create_agent(
    model,
    tools=get_tools_for_state,
    prompt=generate_prompt_with_tools,
)

result = agent.invoke({
    "messages": [("user", "¿Qué puedes hacer por mí?")],
    "user_name": "Ana",
    "user_role": "basic",
})
print(f"\n[basic] {result['messages'][-1].content[:120]}")
# Output esperado:
# [PROMPT] Tools en prompt: ['search', 'calculator']
# 
# [basic] ¡Hola Ana! Puedo ayudarte con dos cosas principales: buscar información general sobre cualquier tema y hacer cálculos ma...

Dynamic prompts basados en contexto avanzado

El prompt puede adaptarse a factores más allá del usuario: hora del día, longitud de la conversación, historial de errores.

Prompt que se adapta a la longitud de la conversación

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es relevante."

def generate_adaptive_prompt(state) -> str:
    """Adapta el prompt según la longitud de la conversación."""
    messages = state.get("messages", [])
    msg_count = len(messages)

    if msg_count <= 2:
        tone = "Da la bienvenida al usuario y sé amigable."
        style = "Sé detallado en tus explicaciones, el usuario está empezando."
    elif msg_count <= 6:
        tone = "El usuario ya está en contexto, no repitas bienvenidas."
        style = "Sé conciso pero informativo."
    else:
        tone = "La conversación es larga. El usuario quiere respuestas directas."
        style = "Sé lo más breve posible. Ve al punto."

    prompt = f"""Eres un asistente técnico.

Contexto de conversación: {msg_count} mensajes intercambiados.
Tono: {tone}
Estilo: {style}

Responde en español."""

    print(f"[PROMPT] {msg_count} mensajes → {'bienvenida' if msg_count <= 2 else 'conciso' if msg_count <= 6 else 'directo'}")
    return prompt

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=[search], prompt=generate_adaptive_prompt)

result = agent.invoke({
    "messages": [("user", "¿Qué es Docker?")],
})
print(f"Respuesta: {result['messages'][-1].content[:100]}")
# Output esperado:
# [PROMPT] 1 mensajes → bienvenida
# Respuesta: ¡Bienvenido! Docker es una plataforma de código abierto que permite crear, desplegar y ejecutar ap...

Prompt basado en hora del día

from dotenv import load_dotenv
load_dotenv()

from datetime import datetime
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es un concepto importante."

def generate_time_aware_prompt(state) -> str:
    """Genera prompt que se adapta a la hora del día."""
    hour = datetime.now().hour

    if 6 <= hour < 12:
        greeting = "Buenos días"
        energy = "El usuario probablemente está empezando su día. Sé energético y motivador."
    elif 12 <= hour < 18:
        greeting = "Buenas tardes"
        energy = "El usuario está en medio de su jornada. Sé eficiente y directo."
    elif 18 <= hour < 22:
        greeting = "Buenas noches"
        energy = "El usuario puede estar cansado. Sé amable y claro."
    else:
        greeting = "Hola"
        energy = "Es muy tarde. Responde de forma ultra-concisa."

    prompt = f"""Eres un asistente amigable. {energy}

Si es la primera interacción, saluda con "{greeting}".
Responde en español."""

    print(f"[PROMPT] Hora: {hour}:00 → {greeting}")
    return prompt

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=[search], prompt=generate_time_aware_prompt)

result = agent.invoke({"messages": [("user", "Hola, ¿qué puedes hacer?")]})
print(f"Respuesta: {result['messages'][-1].content[:100]}")
# Output esperado (varía según hora):
# [PROMPT] Hora: 14:00 → Buenas tardes
# Respuesta: ¡Buenas tardes! Puedo ayudarte a buscar información sobre cualquier tema que necesites. ¿En qué te...

Combinando dynamic tools + dynamic prompts

El verdadero poder aparece cuando ambos cambian juntos: las tools se filtran según el contexto Y el prompt se adapta para describir solo las tools disponibles.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información general."""
    return f"Resultado: {query} es un concepto clave."

@tool
def calculator(expression: str) -> str:
    """Evalúa expresiones matemáticas."""
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

@tool
def generate_report(topic: str) -> str:
    """Genera un reporte detallado sobre un tema (premium)."""
    return f"Reporte sobre '{topic}': análisis completo con 5 secciones."

@tool
def export_data(format: str) -> str:
    """Exporta datos en el formato especificado (premium)."""
    return f"Datos exportados en formato {format}."

@tool
def delete_user(user_id: str) -> str:
    """Elimina un usuario del sistema (admin)."""
    return f"Usuario {user_id} eliminado del sistema."

@tool
def system_config(key: str, value: str) -> str:
    """Modifica configuración del sistema (admin)."""
    return f"Config: {key}={value} actualizada."

ROLE_TOOLS = {
    "basic": [search, calculator],
    "premium": [search, calculator, generate_report, export_data],
    "admin": [search, calculator, generate_report, export_data, delete_user, system_config],
}

ROLE_DESCRIPTIONS = {
    "basic": "Tienes acceso a búsqueda y calculadora.",
    "premium": "Tienes acceso a búsqueda, calculadora, reportes y exportación de datos.",
    "admin": "Tienes acceso completo incluyendo gestión de usuarios y configuración.",
}

def get_dynamic_tools(state) -> list:
    role = state.get("user_role", "basic")
    tools = ROLE_TOOLS.get(role, ROLE_TOOLS["basic"])
    print(f"[TOOLS] rol={role}{[t.name for t in tools]}")
    return tools

def generate_dynamic_prompt(state) -> str:
    role = state.get("user_role", "basic")
    name = state.get("user_name", "usuario")
    tools = ROLE_TOOLS.get(role, ROLE_TOOLS["basic"])
    tool_names = [t.name for t in tools]

    capabilities = ROLE_DESCRIPTIONS.get(role, ROLE_DESCRIPTIONS["basic"])
    tool_list = ", ".join(tool_names)

    prompt = f"""Eres un asistente para {name} con rol "{role}".

Capacidades: {capabilities}
Herramientas disponibles: {tool_list}

Reglas:
- Usa solo las herramientas listadas
- Si piden algo fuera de tus capacidades, explica qué plan necesitan
- Responde en español, sé conciso"""

    print(f"[PROMPT] {name} ({role}) → {len(tools)} tools")
    return prompt

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

agent = create_agent(
    model,
    tools=get_dynamic_tools,
    prompt=generate_dynamic_prompt,
)

print("--- Usuario básico ---")
result_basic = agent.invoke({
    "messages": [("user", "Genera un reporte sobre Python")],
    "user_name": "Laura",
    "user_role": "basic",
})
print(f"[basic] {result_basic['messages'][-1].content[:120]}\n")

print("--- Usuario premium ---")
result_premium = agent.invoke({
    "messages": [("user", "Genera un reporte sobre Python y expórtalo en PDF")],
    "user_name": "Carlos",
    "user_role": "premium",
})
print(f"[premium] {result_premium['messages'][-1].content[:120]}")
# Output esperado:
# --- Usuario básico ---
# [TOOLS] rol=basic → ['search', 'calculator']
# [PROMPT] Laura (basic) → 2 tools
# [basic] Lo siento Laura, la generación de reportes está disponible en el plan premium. Actualmente puedes buscar información y rea...
# 
# --- Usuario premium ---
# [TOOLS] rol=premium → ['search', 'calculator', 'generate_report', 'export_data']
# [PROMPT] Carlos (premium) → 4 tools
# [premium] He generado un reporte sobre Python con un análisis completo de 5 secciones y lo he exportado en formato PDF. ¿Necesitas...

El usuario basic recibe una respuesta que le indica qué plan necesita. El usuario premium obtiene el reporte y la exportación. Todo automático, sin if/else en tu lógica de aplicación.


Patrón avanzado: tools que se agregan según la conversación

Las tools pueden cambiar no solo por el usuario, sino por el estado de la conversación. Por ejemplo, después de buscar información, desbloqueas la tool para generar un resumen.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_core.messages import ToolMessage

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado detallado: {query} es un framework de IA con múltiples componentes y casos de uso en producción."

@tool
def summarize(text: str) -> str:
    """Resume información previamente encontrada."""
    return f"Resumen: {text[:80]}..."

@tool
def deep_analyze(topic: str) -> str:
    """Análisis profundo de un tema (disponible después de buscar)."""
    return f"Análisis: {topic} tiene 3 ventajas clave y 2 limitaciones importantes."

def get_progressive_tools(state) -> list:
    """Tools que se desbloquean según el progreso de la conversación."""
    messages = state.get("messages", [])

    has_search_results = any(
        isinstance(msg, ToolMessage) and "Resultado" in msg.content
        for msg in messages
    )

    base_tools = [search]

    if has_search_results:
        base_tools.extend([summarize, deep_analyze])
        print(f"[PROGRESSIVE] Búsqueda detectada → +summarize, +deep_analyze")
    else:
        print(f"[PROGRESSIVE] Sin búsqueda previa → solo search")

    return base_tools

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_progressive_tools)

result = agent.invoke({
    "messages": [("user", "Busca qué es LangChain y luego hazme un resumen")],
})
print(f"\nRespuesta: {result['messages'][-1].content[:120]}")
# Output esperado:
# [PROGRESSIVE] Sin búsqueda previa → solo search
# [PROGRESSIVE] Búsqueda detectada → +summarize, +deep_analyze
# 
# Respuesta: LangChain es un framework de IA con múltiples componentes y casos de uso en producción. En resumen, se trata de un framew...

La primera iteración del agente solo ve search. Después de buscar, la segunda iteración ve summarize y deep_analyze desbloqueadas.


Consideraciones de seguridad

Dynamic tools introduce un vector de seguridad que debes manejar explícitamente.

Principio: defensa en profundidad

No confíes solo en que el modelo no llame una tool que no le diste. Agrega validación en la tool misma:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

CURRENT_USER = {"role": "basic", "name": "usuario"}

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es un concepto relevante."

@tool
def delete_record(record_id: str) -> str:
    """Elimina un registro de la base de datos. Requiere rol admin."""
    if CURRENT_USER["role"] != "admin":
        return "ERROR: No tienes permisos para eliminar registros."
    return f"Registro {record_id} eliminado."

@tool
def modify_config(key: str, value: str) -> str:
    """Modifica configuración del sistema. Requiere rol admin."""
    if CURRENT_USER["role"] != "admin":
        return "ERROR: No tienes permisos para modificar la configuración."
    return f"Config {key}={value} actualizada."

def get_tools_with_validation(state) -> list:
    role = state.get("user_role", "basic")
    CURRENT_USER["role"] = role
    CURRENT_USER["name"] = state.get("user_name", "usuario")

    if role == "admin":
        return [search, delete_record, modify_config]
    return [search]

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_tools_with_validation)

result = agent.invoke({
    "messages": [("user", "Busca información sobre seguridad en APIs")],
    "user_role": "basic",
    "user_name": "Test",
})
print(f"Respuesta: {result['messages'][-1].content[:100]}")
# Output esperado:
# Respuesta: La seguridad en APIs es un aspecto fundamental del desarrollo de software moderno que abarca autentic...

Reglas de seguridad para dynamic tools

  • Siempre filtra tools por rol — el modelo no debería ver tools que el usuario no puede usar
  • Valida permisos dentro de cada tool — defensa en profundidad, por si el filtrado falla
  • Logea cada invocación de tools sensibles — para auditoría
  • ⚠️ No confíes solo en el prompt — un prompt que dice "no uses delete_record" no es seguridad
  • Nunca expongas tools destructivas a usuarios externos sin doble confirmación
  • Nunca incluyas credenciales o tokens en las tools — usa variables de entorno

Conexión con el proyecto

En el Proyecto del módulo (Cápsula 08), combinarás todo lo aprendido: dynamic model routing (Cápsula 05) con dynamic tools y dynamic prompts para crear un agente que selecciona el modelo según la complejidad, filtra tools por el rol del usuario, y genera un prompt personalizado que describe las capacidades disponibles. AgentMiddleware class (Cápsula 07) te permitirá encapsular todo esto en módulos reutilizables.


Troubleshooting

Problema 1: La función de tools recibe un estado vacío

Causa: No estás pasando los campos necesarios en el input del agente (como user_role). Solución: Asegúrate de incluir los campos en el diccionario de invocación:

result = agent.invoke({
    "messages": [("user", "Pregunta")],
    "user_role": "admin",  # Incluir campos que tu función necesita
})

Problema 2: El prompt dinámico no se aplica

Causa: Pasaste un string en lugar de una función al parámetro prompt. Solución: prompt debe ser un callable (función), no un string:

# Incorrecto
agent = create_agent(model, tools, prompt="Eres un asistente.")

# Correcto para prompt dinámico
agent = create_agent(model, tools, prompt=generate_prompt)

Problema 3: Tools dinámicas causan errores de "tool not found"

Causa: El agente decidió llamar una tool en la primera iteración, pero en la segunda iteración esa tool ya no está disponible porque el estado cambió. Solución: Asegúrate de que tu función de tools retorne un set consistente durante una misma conversación, o incluye manejo de errores:

def get_tools(state):
    tools = [search]  # Siempre incluir las base
    # Solo agregar, nunca quitar durante una conversación
    if state.get("can_analyze"):
        tools.append(analyze)
    return tools

Problema 4: El prompt no refleja las tools disponibles

Causa: La función de prompt y la función de tools no comparten la misma lógica de filtrado. Solución: Usa la misma fuente de verdad para ambas:

ROLE_TOOLS = {"basic": [search], "admin": [search, delete]}

def get_tools(state):
    role = state.get("user_role", "basic")
    return ROLE_TOOLS.get(role, ROLE_TOOLS["basic"])

def generate_prompt(state):
    role = state.get("user_role", "basic")
    tools = ROLE_TOOLS.get(role, ROLE_TOOLS["basic"])  # Misma fuente
    tool_names = ", ".join(t.name for t in tools)
    return f"Herramientas: {tool_names}"

Problema 5: StructuredTool dinámicas no funcionan con el modelo

Causa: La tool creada dinámicamente no tiene un args_schema correcto o su docstring está vacía. Solución: Siempre define un args_schema Pydantic y una descripción clara:

from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool

class MyInput(BaseModel):
    query: str = Field(description="La consulta a ejecutar")

tool = StructuredTool.from_function(
    func=my_func,
    name="my_tool",
    description="Descripción clara de qué hace esta tool",
    args_schema=MyInput,
)

Ejercicios

Ejercicio 1: Tools por rol (Fácil)

Crea un agente con dynamic tools que filtre por dos roles: viewer (solo search) y editor (search + create_note). Prueba con ambos roles.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es un tema de interés."

@tool
def create_note(title: str, content: str) -> str:
    """Crea una nueva nota."""
    return f"Nota '{title}' creada con {len(content)} caracteres."

def get_tools_by_role(state) -> list:
    role = state.get("user_role", "viewer")
    if role == "editor":
        print(f"[TOOLS] editor → [search, create_note]")
        return [search, create_note]
    print(f"[TOOLS] viewer → [search]")
    return [search]

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_tools_by_role)

print("--- Viewer ---")
result = agent.invoke({
    "messages": [("user", "Busca información sobre LangChain")],
    "user_role": "viewer",
})
print(f"Respuesta: {result['messages'][-1].content[:80]}\n")

print("--- Editor ---")
result = agent.invoke({
    "messages": [("user", "Crea una nota titulada 'Resumen' con contenido 'LangChain es genial'")],
    "user_role": "editor",
})
print(f"Respuesta: {result['messages'][-1].content[:80]}")
# Output esperado:
# --- Viewer ---
# [TOOLS] viewer → [search]
# Respuesta: LangChain es un framework de código abierto para construir aplicaciones con mode...
# 
# --- Editor ---
# [TOOLS] editor → [search, create_note]
# Respuesta: He creado la nota titulada 'Resumen' con el contenido 'LangChain es genial'.

Explicación: get_tools_by_role revisa el campo user_role del estado y retorna solo las tools permitidas. El viewer solo puede buscar; el editor también puede crear notas.

Ejercicio 2: Prompt dinámico con nombre (Fácil)

Crea un prompt dinámico que incluya el nombre del usuario y un saludo personalizado según la hora del día (mañana, tarde, noche).

Ver solución
from dotenv import load_dotenv
load_dotenv()

from datetime import datetime
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es un concepto relevante."

def generate_personalized_prompt(state) -> str:
    name = state.get("user_name", "usuario")
    hour = datetime.now().hour

    if 6 <= hour < 12:
        greeting = f"Buenos días, {name}"
    elif 12 <= hour < 20:
        greeting = f"Buenas tardes, {name}"
    else:
        greeting = f"Buenas noches, {name}"

    prompt = f"""Eres un asistente personal. Saluda con "{greeting}" si es la primera interacción.

Responde en español, de forma concisa y amigable."""

    print(f"[PROMPT] Saludo: {greeting}")
    return prompt

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=[search], prompt=generate_personalized_prompt)

result = agent.invoke({
    "messages": [("user", "Hola, ¿qué puedes hacer?")],
    "user_name": "María",
})
print(f"Respuesta: {result['messages'][-1].content[:120]}")
# Output esperado (varía según hora):
# [PROMPT] Saludo: Buenas tardes, María
# Respuesta: ¡Buenas tardes, María! Puedo ayudarte a buscar información sobre cualquier tema que necesites. ¿En qué te puedo ayudar?

Explicación: El prompt se genera en runtime con el nombre del usuario y un saludo apropiado según la hora actual. El modelo recibe este contexto personalizado en cada invocación.

Ejercicio 3: Tier con prompt descriptivo (Medio)

Implementa un sistema de 3 tiers (free, pro, enterprise) donde cada tier tiene diferentes tools Y un prompt que explica qué puede hacer. El usuario free recibe un prompt que menciona las funcionalidades del tier pro como upgrade.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información general."""
    return f"Resultado: {query} encontrado."

@tool
def summarize(text: str) -> str:
    """Resume textos largos (Pro+)."""
    return f"Resumen: {text[:60]}..."

@tool
def translate(text: str, lang: str) -> str:
    """Traduce texto (Pro+)."""
    return f"[{lang}] {text}"

@tool
def generate_report(topic: str) -> str:
    """Genera reportes detallados (Enterprise)."""
    return f"Reporte '{topic}': 10 páginas, 5 gráficos."

@tool
def api_access(endpoint: str) -> str:
    """Acceso directo a la API (Enterprise)."""
    return f"API {endpoint} → 200 OK"

TIER_CONFIG = {
    "free": {
        "tools": [search],
        "upgrade_msg": "\n\nFuncionalidades del plan Pro: resúmenes, traducciones. Menciónalo si el usuario necesita más.",
    },
    "pro": {
        "tools": [search, summarize, translate],
        "upgrade_msg": "\n\nFuncionalidades Enterprise: reportes, acceso API. Menciónalo si aplica.",
    },
    "enterprise": {
        "tools": [search, summarize, translate, generate_report, api_access],
        "upgrade_msg": "",
    },
}

def get_tier_tools(state) -> list:
    tier = state.get("tier", "free")
    tools = TIER_CONFIG.get(tier, TIER_CONFIG["free"])["tools"]
    print(f"[TOOLS] tier={tier}{[t.name for t in tools]}")
    return tools

def generate_tier_prompt(state) -> str:
    tier = state.get("tier", "free")
    name = state.get("user_name", "usuario")
    config = TIER_CONFIG.get(tier, TIER_CONFIG["free"])

    tool_names = ", ".join(t.name for t in config["tools"])

    prompt = f"""Eres un asistente para {name} (plan: {tier}).

Herramientas disponibles: {tool_names}
Usa solo estas herramientas.{config['upgrade_msg']}

Responde en español."""

    print(f"[PROMPT] tier={tier}, tools={tool_names}")
    return prompt

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_tier_tools, prompt=generate_tier_prompt)

print("--- Free ---")
result = agent.invoke({
    "messages": [("user", "Traduce 'hola mundo' al inglés")],
    "user_name": "Pedro",
    "tier": "free",
})
print(f"Respuesta: {result['messages'][-1].content[:120]}\n")

print("--- Pro ---")
result = agent.invoke({
    "messages": [("user", "Traduce 'hola mundo' al inglés")],
    "user_name": "Ana",
    "tier": "pro",
})
print(f"Respuesta: {result['messages'][-1].content[:120]}")
# Output esperado:
# --- Free ---
# [TOOLS] tier=free → ['search']
# [PROMPT] tier=free, tools=search
# Respuesta: Actualmente no tengo la función de traducción disponible en tu plan. Con el plan Pro podrías acceder a traducciones y re...
# 
# --- Pro ---
# [TOOLS] tier=pro → ['search', 'summarize', 'translate']
# [PROMPT] tier=pro, tools=search, summarize, translate
# Respuesta: ¡Listo! La traducción de 'hola mundo' al inglés es: "hello world".

Explicación: Cada tier tiene un set de tools y un prompt que menciona el siguiente tier como upgrade. El usuario free no puede traducir pero recibe sugerencia de upgrade. El usuario pro obtiene la traducción directamente.

Ejercicio 4: Tools progresivas por conversación (Medio)

Crea un sistema donde el agente empieza solo con search, y después de que el usuario haya hecho al menos una búsqueda exitosa, se desbloquean summarize y analyze.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_core.messages import ToolMessage

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} — información detallada encontrada en 3 fuentes."

@tool
def summarize(text: str) -> str:
    """Resume la información encontrada (requiere búsqueda previa)."""
    return f"Resumen: {text[:50]}... (condensado en 2 párrafos)"

@tool
def analyze(topic: str) -> str:
    """Analiza en profundidad un tema (requiere búsqueda previa)."""
    return f"Análisis de '{topic}': 3 pros, 2 contras, tendencia positiva."

def get_progressive_tools(state) -> list:
    messages = state.get("messages", [])

    has_search = any(
        isinstance(msg, ToolMessage) and "Resultado" in msg.content
        for msg in messages
    )

    if has_search:
        tools = [search, summarize, analyze]
        print(f"[PROGRESSIVE] Búsqueda previa detectada → {[t.name for t in tools]}")
    else:
        tools = [search]
        print(f"[PROGRESSIVE] Sin búsqueda → solo [search]")

    return tools

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_progressive_tools)

result = agent.invoke({
    "messages": [("user", "Busca qué es LangChain y analiza sus ventajas")],
})
print(f"\nRespuesta: {result['messages'][-1].content[:120]}")
# Output esperado:
# [PROGRESSIVE] Sin búsqueda → solo [search]
# [PROGRESSIVE] Búsqueda previa detectada → ['search', 'summarize', 'analyze']
# 
# Respuesta: LangChain es un framework para aplicaciones con LLMs. Su análisis muestra 3 pros, 2 contras y una tendencia positiva en...

Explicación: La primera iteración del agente solo puede buscar. Después de obtener resultados, la segunda iteración detecta el ToolMessage con "Resultado" y desbloquea summarize y analyze.

Ejercicio 5: Dynamic tools + dynamic prompt + logging (Difícil)

Crea un sistema completo que combine: tools filtradas por rol, prompt personalizado por nombre y rol, y un log de cada invocación que muestre qué tools se proporcionaron y qué prompt se generó.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Busca información general."""
    return f"Resultado: {query} es un concepto importante."

@tool
def calculator(expression: str) -> str:
    """Evalúa expresiones matemáticas."""
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

@tool
def export_csv(data: str) -> str:
    """Exporta datos a CSV (requiere rol editor+)."""
    return f"CSV exportado: {len(data)} caracteres."

@tool
def manage_users(action: str, user_id: str) -> str:
    """Gestiona usuarios del sistema (solo admin)."""
    return f"Acción '{action}' ejecutada para usuario {user_id}."

ROLE_CONFIG = {
    "viewer": {
        "tools": [search],
        "description": "Puedes buscar información.",
    },
    "editor": {
        "tools": [search, calculator, export_csv],
        "description": "Puedes buscar, calcular y exportar datos.",
    },
    "admin": {
        "tools": [search, calculator, export_csv, manage_users],
        "description": "Tienes acceso completo al sistema.",
    },
}

invocation_log = []

def get_tools(state) -> list:
    role = state.get("user_role", "viewer")
    config = ROLE_CONFIG.get(role, ROLE_CONFIG["viewer"])
    return config["tools"]

def generate_prompt(state) -> str:
    role = state.get("user_role", "viewer")
    name = state.get("user_name", "usuario")
    config = ROLE_CONFIG.get(role, ROLE_CONFIG["viewer"])
    tools = config["tools"]
    tool_names = [t.name for t in tools]

    prompt = f"""Eres un asistente para {name} (rol: {role}).
{config['description']}
Herramientas: {', '.join(tool_names)}
Responde en español, de forma concisa."""

    invocation_log.append({
        "user": name,
        "role": role,
        "tools_count": len(tools),
        "tools": tool_names,
        "prompt_length": len(prompt),
    })
    print(f"[LOG] {name} ({role}) → {len(tools)} tools, prompt={len(prompt)} chars")
    return prompt

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_tools, prompt=generate_prompt)

test_users = [
    {"name": "Luis", "role": "viewer", "msg": "Busca qué es Docker"},
    {"name": "Ana", "role": "editor", "msg": "¿Cuánto es 15 * 23?"},
    {"name": "Admin", "role": "admin", "msg": "Desactiva al usuario U-789"},
]

for user in test_users:
    print(f"\n--- {user['name']} ({user['role']}) ---")
    result = agent.invoke({
        "messages": [("user", user["msg"])],
        "user_name": user["name"],
        "user_role": user["role"],
    })
    print(f"Respuesta: {result['messages'][-1].content[:80]}")

print(f"\n{'='*55}")
print(f"  LOG DE INVOCACIONES")
print(f"{'='*55}")
for entry in invocation_log:
    print(f"  {entry['user']:>6} ({entry['role']:>6}) → {entry['tools_count']} tools: {entry['tools']}")
# Output esperado:
# --- Luis (viewer) ---
# [LOG] Luis (viewer) → 1 tools, prompt=120 chars
# Respuesta: Docker es una plataforma de contenedorización que permite empaquetar aplicaciones...
# 
# --- Ana (editor) ---
# [LOG] Ana (editor) → 3 tools, prompt=155 chars
# Respuesta: 15 × 23 = 345.
# 
# --- Admin (admin) ---
# [LOG] Admin (admin) → 4 tools, prompt=170 chars
# Respuesta: La acción de desactivación se ha ejecutado para el usuario U-789.
# 
# =======================================================
#   LOG DE INVOCACIONES
# =======================================================
#    Luis (viewer) → 1 tools: ['search']
#     Ana (editor) → 3 tools: ['search', 'calculator', 'export_csv']
#   Admin ( admin) → 4 tools: ['search', 'calculator', 'export_csv', 'manage_users']

Explicación: El sistema combina tres dinamismos: tools por rol, prompt personalizado, y logging de cada invocación. El log muestra exactamente qué configuración recibió cada usuario, útil para auditoría.

Ejercicio 6: Sistema con MCP tools dinámicas (Difícil)

Simula un sistema donde el usuario puede "conectar" APIs externas. Cada API conectada agrega una tool dinámica al agente. El prompt se actualiza para describir las APIs disponibles.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import StructuredTool
from langchain_core.tools import tool
from pydantic import BaseModel, Field

@tool
def search(query: str) -> str:
    """Busca información general."""
    return f"Resultado: {query} es relevante."

class APICallInput(BaseModel):
    endpoint: str = Field(description="Endpoint de la API")
    params: str = Field(default="", description="Parámetros de la consulta")

API_CATALOG = {
    "weather": {
        "name": "Weather API",
        "description": "Consulta clima actual y pronóstico",
        "base_url": "https://api.weather.example.com",
    },
    "news": {
        "name": "News API",
        "description": "Busca noticias recientes por tema",
        "base_url": "https://api.news.example.com",
    },
    "stocks": {
        "name": "Stocks API",
        "description": "Consulta precios y tendencias de acciones",
        "base_url": "https://api.stocks.example.com",
    },
    "translate": {
        "name": "Translation API",
        "description": "Traduce texto entre idiomas",
        "base_url": "https://api.translate.example.com",
    },
}

def create_api_tool(api_key: str) -> StructuredTool:
    api = API_CATALOG[api_key]

    def call_api(endpoint: str, params: str = "") -> str:
        url = f"{api['base_url']}{endpoint}"
        return f"[{api['name']}] GET {url}?{params} → 200 OK (datos simulados para {api_key})"

    return StructuredTool.from_function(
        func=call_api,
        name=f"call_{api_key}",
        description=f"{api['description']}. Base URL: {api['base_url']}",
        args_schema=APICallInput,
    )

def get_mcp_tools(state) -> list:
    connected = state.get("connected_apis", [])
    tools = [search]

    for api_key in connected:
        if api_key in API_CATALOG:
            tools.append(create_api_tool(api_key))

    tool_names = [t.name for t in tools]
    print(f"[MCP] APIs conectadas: {connected} → tools: {tool_names}")
    return tools

def generate_mcp_prompt(state) -> str:
    connected = state.get("connected_apis", [])
    name = state.get("user_name", "usuario")

    api_descriptions = []
    for api_key in connected:
        if api_key in API_CATALOG:
            api = API_CATALOG[api_key]
            api_descriptions.append(f"- {api['name']}: {api['description']}")

    apis_text = "\n".join(api_descriptions) if api_descriptions else "- Ninguna API externa conectada"

    available_apis = [k for k in API_CATALOG if k not in connected]
    suggestion = ""
    if available_apis:
        suggestion = f"\n\nAPIs disponibles para conectar: {', '.join(available_apis)}"

    return f"""Eres un asistente para {name}.

Además de búsqueda general, tienes acceso a estas APIs:
{apis_text}
{suggestion}
Responde en español."""

model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools=get_mcp_tools, prompt=generate_mcp_prompt)

print("--- Sin APIs ---")
result = agent.invoke({
    "messages": [("user", "¿Qué tiempo hace en Madrid?")],
    "user_name": "Carlos",
    "connected_apis": [],
})
print(f"Respuesta: {result['messages'][-1].content[:100]}\n")

print("--- Con Weather + News ---")
result = agent.invoke({
    "messages": [("user", "¿Qué tiempo hace en Madrid y cuáles son las noticias del día?")],
    "user_name": "Carlos",
    "connected_apis": ["weather", "news"],
})
print(f"Respuesta: {result['messages'][-1].content[:120]}")
# Output esperado:
# --- Sin APIs ---
# [MCP] APIs conectadas: [] → tools: ['search']
# Respuesta: No tengo acceso directo a datos meteorológicos en tiempo real, pero puedo buscar información general...
# 
# --- Con Weather + News ---
# [MCP] APIs conectadas: ['weather', 'news'] → tools: ['search', 'call_weather', 'call_news']
# Respuesta: Según la Weather API, el tiempo en Madrid muestra condiciones favorables. En cuanto a las noticias del día, la News API r...

Explicación: Las tools se crean dinámicamente usando StructuredTool.from_function para cada API conectada. El prompt se adapta para describir las APIs disponibles y sugerir las que aún no están conectadas. Este patrón simula cómo funcionan los servidores MCP en producción.


Resumen

En esta cápsula aprendiste:

  • Dynamic tools filtran las herramientas disponibles según el estado: rol de usuario, tier, entorno, o progreso de la conversación
  • Pasar una función en lugar de una lista al parámetro tools de create_agent activa el filtrado dinámico
  • Dynamic prompts adaptan el system prompt en runtime: nombre del usuario, hora del día, tools disponibles, longitud de la conversación
  • Pasar un callable al parámetro prompt de create_agent activa la generación dinámica
  • Combinar ambos es el patrón más poderoso: las tools cambian Y el prompt describe exactamente qué puede hacer el agente
  • StructuredTool.from_function permite crear tools en runtime para APIs descubiertas dinámicamente (patrón MCP)
  • Seguridad: siempre filtra por rol + valida dentro de cada tool (defensa en profundidad)
  • Nunca confíes solo en el prompt para seguridad — el modelo puede ignorar instrucciones

Próxima cápsula: AgentMiddleware class — cómo componer múltiples middleware (model routing + dynamic tools + logging) en módulos reutilizables usando la clase AgentMiddleware.


Recursos adicionales

  1. How to use dynamic tools in agents — Guía oficial de dynamic tools
  2. How to use dynamic prompts — System prompts dinámicos en agentes
  3. create_agent API Reference — Parámetros tools y prompt
  4. StructuredTool API Reference — Crear tools programáticamente
  5. MCP Integration Guide — Conectar servidores MCP como tools dinámicas
  6. Security Best Practices for Agents — Seguridad en agentes con tools

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