Deep Research con LangGraph (1/3): agentes Scope y Researcher

Deep Research con LangGraph (1/3): agentes Scope y Researcher

📚 **Esta entrada es parte de la serie _Deep Research con LangGraph_**, dividida en tres capítulos que se leen en orden:

> * 👉 **Parte 1: Agentes Scope y Researcher**

* Parte 2: Research Supervisor multiagente

* Parte 3: Agente Writer e informe final

¿Alguna vez has ido a buscar el mejor algo? ¿Las mejores zapatillas para correr, la mejor herramienta para esa reparación, la mejor barbacoa?

Cuando vas a buscar de las 80 páginas que aparecen, el 90 % son páginas para ganar dinero con afiliados de Amazon, hechas por gente que no sabe de lo que está escribiendo, solo busca los productos que más se venden en Amazon, llenan la página de palabras clave para aparecer en los primeros puestos de búsqueda de Google y llenan el contenido de enlaces hacia Amazon.

En el 10 % que queda tienes que navegar por páginas con parrafadas inmensas, hechas para mejorar el SEO y aparecer en los primeros puestos de búsqueda de Google.

Por lo que una simple búsqueda en Google se convierte en un trabajo pesado y aburrido.

Otra cosa es cuando quieres investigar sobre un tema que no requiere compras. Al igual que antes, tienes que navegar por docenas de páginas, contrastando información, siguiendo fuentes, etc.

¿Y si le encargamos eso a un asistente de IA?

En este post vamos a explicar cómo crear tu propio asistente de IA para que investigue por ti sobre cualquier tema y así dediques tu tiempo a lo que realmente importa.

**Información**: Este post se basa en el curso Deep Research with LangGraph de LangChain Academy. Pero como sentía que había muchos conceptos que se dan por supuestos, lo he escrito de la manera que me hubiera gustado encontrarlo

¿Qué es un asistente de IA?link image 1

Un asistente de IA es un agente que puede realizar tareas complejas y automatizadas. Pero en la época actual del desarrollo de agentes, tenemos que diseñarlo muy bien para que no se pierda por el agujero de la madriguera como Alicia en el país de las maravillas y sepa cuándo parar de buscar y cuándo ya ha encontrado la información que necesitabas.

Arquitectura del asistentelink image 2

Para realizar el asistente vamos a usar la siguiente arquitectura:

deep researcher architecture

Scopelink image 3

Primero vamos a definir el alcance de la investigación. Un LLM analizará el mensaje del usuario y, si considera que necesita más información, se la pedirá.

Researcher supervisorlink image 4

Aquí un LLM se encargará de supervisar la investigación, dividiéndola en pequeñas investigaciones y asignándoselas a subagentes investigadores. Cada uno de esos subagentes tendrá herramientas para poder realizar la investigación.

Cada subagente investigador usará sus herramientas varias veces para obtener una investigación profunda de su tarea y cuando termine le devolverá al supervisor un resumen de su investigación. Cada subagente tiene su propio contexto y no tiene acceso al contexto y resultados de los otros subagentes.

En cuanto el supervisor reciba todos los resúmenes de los subagentes, evaluará si tiene que pedir nuevas investigaciones a subagentes. En el momento en que considere que ya no es necesario realizar más investigaciones, realizará un resumen de toda la información que tiene y se la enviará al writer.

Writerlink image 5

Por último, cuando ya se ha realizado la investigación profunda, el writer se encarga de escribir un informe completo y detallado para el usuario.

Instalación de libreríaslink image 6

Vamos a instalar todas las librerías que vamos a necesitar para el desarrollo del asistente.

Primero creamos un entorno uv

	
< > Input
Python
!uv init deepresearcher
Copied
>_ Output
			
error: Project is already initialized in `/Users/macm1/Documents/web/portafolio/posts/deepresearcher` (`pyproject.toml` file exists)

Lo sincronizamos

	
< > Input
Python
!cd deepresearcher && uv sync
Copied
>_ Output
			
Resolved 88 packages in 9ms
Audited 84 packages in 0.38ms

Lo activamos

	
< > Input
Python
!cd deepresearcher && source .venv/bin/activate
Copied

Instalamos las librerías necesarias

	
< > Input
Python
!cd deepresearcher && source .venv/bin/activate && uv add "alive-progress&gt;=3.3.0" "dotenv&gt;=0.9.9" "grandalf&gt;=0.8" "ipykernel&gt;=6.30.1" "langchain&gt;=0.3.27" "langchain-anthropic&gt;=0.3.21" "langchain-community&gt;=0.3.29" "langchain-mcp-adapters&gt;=0.1.11" "langchain-openai&gt;=0.3.33" "langgraph&gt;=0.6.7" "rich&gt;=14.1.0" "tavily-python&gt;=0.7.12"
Copied
>_ Output
			
Resolved 88 packages in 0.59ms
Audited 84 packages in 0.01ms

Vamos a analizar qué hemos instalado

Utilslink image 7

Antes de meternos en el código, vamos a crear algunas funciones que nos van a ser útiles para el desarrollo del asistente.

Directorylink image 8

En primer lugar, creamos una función que nos permite obtener el directorio actual.

	
< > Input
Python
from pathlib import Path
def get_current_dir() -&gt; Path:
"""Get the current directory of the module.
This function is compatible with Jupyter notebooks and regular Python scripts.
Returns:
Path object representing the current directory
"""
try:
return Path(__file__).resolve().parent
except NameError: # __file__ is not defined
return Path.cwd()
Copied

Datelink image 9

Ahora creamos una función que nos permite obtener la fecha actual.

	
< > Input
Python
from datetime import datetime
def get_today_str() -&gt; str:
"""Get current date in a human-readable format."""
return datetime.now().strftime("%a %b %-d, %Y")
Copied

Messageslink image 10

Por último, creamos una serie de funciones que nos permiten formatear los mensajes para que se muestren de una manera más clara en la consola

	
< > Input
Python
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
import json
import ast
console = Console()
def format_tool_outputs(tool_outputs_list: list) -&gt; str:
"""Format a list of ToolMessage objects into a readable string"""
formatted_parts = []
for i, output in enumerate(tool_outputs_list, 1):
# Handle ToolMessage objects that have been converted to strings
if isinstance(output, dict):
formatted_parts.append(f" Tool Output {i}:")
formatted_parts.append(f" Name: {output.get('name', 'N/A')}")
formatted_parts.append(f" Tool Call ID: {output.get('tool_call_id', 'N/A')}")
# Format the content (which contains the actual output)
content = output.get('content', 'N/A')
if isinstance(content, str):
# Truncate very long outputs
if len(content) &gt; 500:
content_preview = content[:500] + f"... ({len(content)} chars total)"
else:
content_preview = content
formatted_parts.append(f" Content:")
# Indent each line of content
for line in content_preview.split(' '):
formatted_parts.append(f" {line}")
else:
formatted_parts.append(f" Content: {content}")
else:
formatted_parts.append(f" Tool Output {i}: {output}")
return " ".join(formatted_parts)
def format_string_with_structures(content: str) -&gt; str:
"""
Detect and format structured data (lists, dicts) within a string.
Handles cases like 'Tool calls: [...]' and 'Tool outputs: [...]' by parsing and formatting the structure.
"""
# Check if the string contains 'Tool outputs:' followed by a list
if 'Tool outputs:' in content:
try:
# Find the position where the tool outputs list starts
tool_outputs_index = content.index('Tool outputs:')
prefix = content[:tool_outputs_index + len('Tool outputs:')]
# Extract the part that should be a list
potential_list = content[tool_outputs_index + len('Tool outputs:'):].strip()
# Try to parse it as a Python literal (list/dict)
parsed_data = ast.literal_eval(potential_list)
# If it's a list of tool outputs, format them nicely
if isinstance(parsed_data, list):
formatted_parts = [prefix + " "]
formatted_parts.append(format_tool_outputs(parsed_data))
return " ".join(formatted_parts)
except (ValueError, SyntaxError):
# If parsing fails, continue to next check
pass
# Check if the string contains 'Tool calls:' followed by a list
if 'Tool calls:' in content:
try:
# Find the position where the tool calls list starts
tool_calls_index = content.index('Tool calls:')
prefix = content[:tool_calls_index + len('Tool calls:')]
# Extract the part that should be a list
potential_list = content[tool_calls_index + len('Tool calls:'):].strip()
# Try to parse it as a Python literal (list/dict)
parsed_data = ast.literal_eval(potential_list)
# If it's a list of tool calls, format them nicely
if isinstance(parsed_data, list):
formatted_parts = [prefix + " "]
for i, tool_call in enumerate(parsed_data, 1):
if isinstance(tool_call, dict):
formatted_parts.append(f" Tool Call {i}:")
formatted_parts.append(f" Name: {tool_call.get('name', 'N/A')}")
formatted_parts.append(f" ID: {tool_call.get('id', 'N/A')}")
formatted_parts.append(f" Type: {tool_call.get('type', 'N/A')}")
if 'args' in tool_call:
formatted_parts.append(f" Args:")
formatted_parts.append(f" {json.dumps(tool_call['args'], indent=7, ensure_ascii=False)}")
return " ".join(formatted_parts)
except (ValueError, SyntaxError):
# If parsing fails, return the original content
pass
# Try to detect if the entire content is a JSON/dict/list
try:
# Try JSON first
parsed = json.loads(content)
return json.dumps(parsed, indent=2, ensure_ascii=False)
except (json.JSONDecodeError, TypeError):
try:
# Try Python literal (ast.literal_eval)
parsed = ast.literal_eval(content)
return json.dumps(parsed, indent=2, ensure_ascii=False)
except (ValueError, SyntaxError):
# If all parsing attempts fail, return original content
return content
def format_message_content(message):
"""Convert message content to displayable string"""
parts = []
tool_calls_processed = False
# Handle ToolCall objects (special case - they don't have 'content')
if isinstance(message, dict) and 'args' in message and 'name' in message:
# This is a ToolCall dictionary
parts.append(f"Tool Call Name: {message.get('name', 'N/A')}")
parts.append(f"ID: {message.get('id', 'N/A')}")
# Format the args (which should be the tool calls list)
args = message.get('args', [])
if isinstance(args, list):
for i, tool_call in enumerate(args, 1):
if isinstance(tool_call, dict):
parts.append(f" Tool Call {i}:")
parts.append(f" Name: {tool_call.get('name', 'N/A')}")
parts.append(f" ID: {tool_call.get('id', 'N/A')}")
parts.append(f" Type: {tool_call.get('type', 'N/A')}")
if 'args' in tool_call:
parts.append(f" Args:")
parts.append(f" {json.dumps(tool_call['args'], indent=7, ensure_ascii=False)}")
return " ".join(parts)
# Check if message has 'content' attribute
if not hasattr(message, 'content'):
# If it's a dict-like object, try to access it as a dict
if hasattr(message, '__getitem__'):
try:
content = message['content'] if 'content' in message else str(message)
parts.append(content)
return " ".join(parts)
except (KeyError, TypeError):
parts.append(str(message))
return " ".join(parts)
else:
parts.append(str(message))
return " ".join(parts)
# Handle main content (normal messages with 'content' attribute)
if isinstance(message.content, str):
# Try to detect and format structured data within the string
formatted_content = format_string_with_structures(message.content)
parts.append(formatted_content)
elif isinstance(message.content, list):
# Handle complex content like tool calls (Anthropic format)
for item in message.content:
if item.get('type') == 'text':
parts.append(item['text'])
elif item.get('type') == 'tool_use':
parts.append(f" 🔧 Tool Call: {item['name']}")
parts.append(f" Args: {str(json.dumps(item['input'], indent=7, ensure_ascii=False))}")
parts.append(f" ID: {item.get('id', 'N/A')}")
tool_calls_processed = True
else:
parts.append(str(message.content))
# Handle tool calls attached to the message (OpenAI format) - only if not already processed
if not tool_calls_processed and hasattr(message, 'tool_calls') and message.tool_calls:
for tool_call in message.tool_calls:
parts.append(f" 🔧 Tool Call: {tool_call['name']}")
parts.append(f" Args: {json.dumps(tool_call['args'], indent=2)}")
parts.append(f" ID: {tool_call['id']}")
return " ".join(parts)
def format_messages(messages, title: str = "", border_style: str = "white", msg_subtype: str = ""):
"""Format and display a list of messages with Rich formatting
Args:
messages: List of messages to format
title: Title of the panel
border_style: Border style of the panel
msg_subtype: Subtype of the message
"""
# Check if messages is a list
if isinstance(messages, list):
for m in messages:
# Handle different message types
if isinstance(m, dict):
# Check if it's a ToolCall dict
if 'args' in m and 'name' in m:
msg_type = 'ToolCall'
else:
msg_type = 'Dict'
else:
msg_type = m.__class__.__name__.replace('Message', '')
content = format_message_content(m)
if msg_type == 'Human':
if msg_subtype == 'RealHumanMessage':
if title == "":
title = "🧑 Real Human Message"
else:
title = f"🧑 {title}"
console.print(Panel(content, title=title, border_style="#201ADB")) # Blue
else:
if title == "":
title = "🧑 Simulated Human Message"
else:
title = f"🧑 {title}"
console.print(Panel(content, title=title, border_style="#1A64DB")) # Blue
elif msg_type == 'AI':
if title == "":
title = "🤖 Assistant"
else:
title = f"🤖 {title}"
console.print(Panel(content, title=title, border_style="#24FA00")) # Green
elif msg_type == 'ClarifyWithUser':
if title == "":
title = "🤖 Clarify With User"
else:
title = f"🤖 {title}"
console.print(Panel(content, title=title, border_style="#37DB1A")) # Green
elif msg_type == 'ResearchQuestion':
if title == "":
title = "🤖 Research brief generated"
else:
title = f"🤖 {title}"
console.print(Panel(content, title=title, border_style="#37DB1A")) # Green
elif msg_type == 'Tool':
if title == "":
title = "🔧 Tool Output"
else:
title = f"🔧 {title}"
console.print(Panel(content, title=title, border_style="yellow"))
elif msg_type == 'ToolCall':
if title == "":
title = "🔧 Tool Calls"
else:
title = f"🔧 {title}"
console.print(Panel(content, title=title, border_style="magenta"))
elif msg_type == 'Use tools':
if title == "":
title = "🔧 Tool Output"
else:
title = f"🔧 {title}"
console.print(Panel(content, title=title, border_style="yellow"))
elif msg_type == 'System':
if title == "":
title = "🔧 System Message"
else:
title = f"🔧 {title}"
console.print(Panel(content, title=title, border_style="red"))
else:
print(f"msg_type: {msg_type}")
if title == "":
title = f"📝 {msg_type}"
else:
title = f"📝 {title}"
console.print(Panel(content, title=title, border_style="white"))
elif isinstance(messages, str):
console.print(Panel(messages, title=title, border_style=border_style))
else:
print(f"messages type: {type(messages)}")
Copied

Scopelink image 11

Vamos a empezar a diseñar el primer módulo, el de scope. Con este módulo vamos a definir el alcance de la investigación.

Scope promptslink image 12

Vamos a empezar definiendo los prompts del scope. Primero definimos el prompt que hace que el LLM pida más información al usuario cuando considera que no tiene suficiente información.

  • En primer lugar se le pasa el contexto de la conversación que se haya tenido hasta ese momento
  • Se le pasa la fecha, por eso hemos tenido que crear una función para obtener la fecha
  • Se le indica que haga preguntas aclaratorias al usuario, si es necesario. Si el usuario pide investigar sobre algo y el LLM considera que necesita más información de lo que el usuario quiere, puede preguntarle por más información. Si no necesita más información, le tiene que indicar al usuario que tiene todo lo que necesita para empezar la investigación
  • Se le indica el formato de la salida que tiene que responder
	
< > Input
Python
clarify_with_user_instructions="""
Estos son los mensajes que se han intercambiado hasta ahora con el usuario que solicita el informe:
&lt;Messages&gt;
{messages}
&lt;/Messages&gt;
La fecha de hoy es {date}.
Evalúa si necesitas hacer alguna pregunta aclaratoria o si el usuario ya te ha proporcionado suficiente información para comenzar la investigación.
IMPORTANTE: Si en el historial de mensajes ves que ya has hecho una pregunta aclaratoria, casi siempre no es necesario que hagas otra. Solo haz otra pregunta si es ABSOLUTAMENTE NECESARIO.
Si hay acrónimos, abreviaturas o términos desconocidos, pide al usuario que los aclare.
Si necesitas hacer una pregunta, sigue estas pautas:
- Se conciso al recopilar toda la información necesaria.
- Asegúrate de recopilar toda la información necesaria para llevar a cabo la tarea de investigación de forma concisa y bien estructurada.
- Utiliza bullet points o listas numeradas si es necesario para mayor claridad. Asegúrate de que utilizas el formato Markdown y de que se mostrará correctamente si la cadena de salida se pasa a un renderizador Markdown.
- No solicites información innecesaria o información que el usuario ya haya proporcionado. Si ves que el usuario ya ha proporcionado la información, no se la vuelvas a solicitar.
Responde en formato JSON válido con estas claves exactas:
"need_clarification": boolean,
"question": "&lt;pregunta para pedir al usuario que aclare el alcance del informe&gt;",
"verification": "&lt;mensaje de verificación de que comenzaremos la investigación&gt;"
Si necesitas hacer una pregunta aclaratoria, devuelve:
"need_clarification": true,
"question": "&lt;tu pregunta aclaratoria&gt;",
"verification": ""
Si no necesitas hacer una pregunta aclaratoria, devuelve:
"need_clarification": false,
"question": "",
"verification": "&lt;Mensaje de confirmación de que ahora comenzará la investigación basándose en la información proporcionada.&gt;"
Para el mensaje de verificación cuando no se necesita aclaración:
- Confirma que tienes suficiente información para continuar.
- Resume brevemente los aspectos clave de lo que entiendes de la solicitud.
- Confirma que ahora comenzará el proceso de investigación.
- Mantén el mensaje conciso y profesional.
Responde siempre en el idioma en el que el usuario te haya preguntado. En caso de duda, responde en español.
"""
Copied

Ahora definimos el prompt para que, con la conversación que el LLM ha tenido con el usuario, el LLM genere un prompt con la investigación que se tiene que realizar y que irá al agente de investigación.

  • Se le pasa el contexto de la conversación que se haya tenido hasta ese momento
  • Se le indica la fecha
  • Se indica cómo tiene que ser el formato del prompt que tiene que generar. No se pueden asumir cosas que el usuario no ha dicho. Tiene que ser específico y detallado. Escribirlo en primera persona
	
< > Input
Python
transform_messages_into_research_topic_prompt = """Se te va a proporcionar un conjunto de mensajes que se han intercambiado hasta ahora entre el usuario y tú.
Tu trabajo consiste en traducir estos mensajes en una pregunta de investigación más detallada y concreta que se utilizará para orientar la investigación.
Los mensajes que se han intercambiado hasta ahora entre el usuario y tú son:
&lt;Messages&gt;
{messages}
&lt;/Messages&gt;
La fecha de hoy es {date}.
Devolverás una única pregunta de investigación que se utilizará para orientar la investigación.
Directrices:
1. Maximiza la especificidad y el detalle.
- Incluye todas las preferencias conocidas del usuario y enumera explícitamente los atributos o dimensiones clave a tener en cuenta.
- Es importante que todos los detalles del usuario se incluyan en las instrucciones.
2. Trata con cuidado las dimensiones no especificadas
- Cuando la calidad de la investigación requiera tener en cuenta dimensiones adicionales que el usuario no haya especificado, reconócelas como consideraciones abiertas en lugar de preferencias asumidas.
- Ejemplo: en lugar de asumir «opciones económicas», di «considerar todos los rangos de precios a menos que se especifiquen restricciones de coste».
- Solo menciona las dimensiones que sean realmente necesarias para una investigación exhaustiva en ese ámbito.
3. Evita suposiciones injustificadas
- Nunca inventes preferencias, restricciones o requisitos específicos del usuario que no se hayan indicado.
- Si el usuario no ha proporcionado un detalle concreto, señala explícitamente esta falta de especificación.
- Orienta al investigador para que trate los aspectos no especificados como flexibles en lugar de hacer suposiciones.
4. Distinguir entre el alcance de la investigación y las preferencias del usuario
- Alcance de la investigación: qué temas/dimensiones deben investigarse (puede ser más amplio que las menciones explícitas del usuario)
- Preferencias del usuario: restricciones, requisitos o preferencias específicos (solo debes incluir lo que el usuario haya indicado)
- Ejemplo: «Investigar los factores que influyen en la calidad del café (incluido el origen de los granos, los métodos de tostado y las técnicas de preparación) en las cafeterías de Madrid, centrándose principalmente en el sabor, tal y como ha especificado el usuario».
5. Utiliza la primera persona
- Redacta la solicitud desde la perspectiva del usuario.
6. Fuentes
- Si se debe dar prioridad a fuentes específicas, entonces específalas en la pregunta de investigación.
- Para la investigación de productos y viajes, es preferible enlazar directamente con sitios web oficiales o primarios (por ejemplo, sitios web oficiales de marcas, páginas de fabricantes o plataformas de comercio electrónico de renombre como Amazon para opiniones de usuarios) en lugar de sitios agregadores o blogs con mucho SEO.
- Para consultas académicas o científicas, es preferible enlazar directamente con el artículo original o la publicación oficial en una revista, en lugar de con artículos de encuestas o resúmenes secundarios.
- En el caso de personas, intente enlazar directamente con su perfil de LinkedIn, su sitio web personal o su perfil de GitHub, si lo tienen.
- Si la consulta está en un idioma específico, dé prioridad a las fuentes publicadas en ese idioma.
Responde siempre en el idioma en el que el usuario te haya preguntado. En caso de duda, responde en español.
"""
Copied

Scope statelink image 13

Definimos ahora el estado del scope. El estado es como la memoria a corto plazo del agente, donde se van a guardar las variables y datos que se van a usar durante la ejecución del agente y que tienen que estar disponibles durante la ejecución del agente.

En cuanto el agente deje de ejecutarse, dicha memoria se perderá

Vamos a definir dos estados

	
< > Input
Python
import operator
from typing_extensions import Optional, Annotated, Sequence
from langchain_core.messages import BaseMessage
from langgraph.graph import MessagesState
from langgraph.graph.message import add_messages
class AgentInputState(MessagesState):
"""Input state for the full agent - only contains messages from user input."""
pass
class AgentState(MessagesState):
"""
Main state for the full multi-agent research system.
Extends MessagesState with additional fields for research coordination.
Note: Some fields are duplicated across different state classes for proper
state management between subgraphs and the main workflow.
"""
# Research brief generated from user conversation history
research_brief: Optional[str]
# Messages exchanged with the supervisor agent for coordination
supervisor_messages: Annotated[Sequence[BaseMessage], add_messages]
# Raw unprocessed research notes collected during the research phase
raw_notes: Annotated[list[str], operator.add] = []
# Processed and structured notes ready for report generation
notes: Annotated[list[str], operator.add] = []
# Final formatted research report
final_report: str
Copied

Puede parecer raro crear dos estados, AgentInputState y AgentState. Podríamos haber creado un solo estado que se llamara AgentState, ya que los dos que hemos creado heredan de la clase MessagesState. Por lo que, definiendo solo uno o definiendo dos, los mensajes que se vayan generando, tanto los que vienen del usuario como los que genera el agente, se van a guardar en una lista llamada messages.

Pero lo hemos hecho en dos, porque vamos a definir AgentInputState como el formato de entrada que va a esperar el agente, y AgentState como el estado interno del agente. ¿Para qué? Para que el resto de variables del estado (research_brief, supervisor_messages, raw_notes, notes, final_report) no se puedan modificar cuando se haga una llamada al agente.

Por ejemplo, si hubiésemos creado solo un estado AgentState, a la hora de invocar al agente, podríamos hacer

state = {
"messages": ["Hola, ¿cómo estás?"],
"research_brief": "Investigar los factores que influyen en la calidad del café",
"supervisor_messages": [],
"raw_notes": [],
"notes": [],
"final_report": ""
}

agent.invoke(state)

Al hacer esto, inicialmente habría un valor para research_brief, y no queremos eso, queremos que sea el agente el que genere el research_brief.

Al tener un estado AgentState y otro AgentInputState, el estado AgentState, vamos a establecer como variable al invocar al agente a AgentInputState, de modo que no vamos a poder modificar el research_brief desde fuera del agente.

Salida estructurada del agente Scopelink image 14

Definimos ahora cómo queremos que sea la salida del LLM cuando se le invoque. Definimos dos salidas estructuradas, una para cuando queremos saber si necesitamos más datos del usuario (ClarifyWithUser) y otra para cuando queremos generar el research_brief (ResearchQuestion).

	
< > Input
Python
from pydantic import BaseModel, Field
class ClarifyWithUser(BaseModel):
"""Schema for user clarification decision and questions."""
need_clarification: bool = Field(
description="Whether the user needs to be asked a clarifying question.",
)
question: str = Field(
description="A question to ask the user to clarify the report scope",
)
verification: str = Field(
description="Verify message that we will start research after the user has provided the necessary information.",
)
class ResearchQuestion(BaseModel):
"""Schema for structured research brief generation."""
research_brief: str = Field(
description="A research question that will be used to guide the research.",
)
Copied

La función Field de pydantic se usa para definir el tipo de dato que se espera a la salida del LLM y para añadir una descripción de lo que se espera que sea la salida.

Con ClarifyWithUser se espera que el LLM devuelva

  • un booleano (need_clarification) que indique si se necesita más información del usuario
  • una pregunta (question) que en caso de que se necesite más información del usuario, se le va a hacer para aclarar el alcance de la investigación
  • un mensaje de verificación (verification) que se le va a dar al usuario para que confirme que tiene suficiente información para comenzar la investigación

Con ResearchQuestion se espera que el LLM devuelva una pregunta de investigación (research_brief), que es la que se le va a mandar al agente de investigación para que se haga la investigación.

Modelo de lenguaje del agente Scopelink image 15

Ahora que tenemos los prompts, definimos el modelo de lenguaje que va a usar el agente.

	
< > Input
Python
from langchain.chat_models import init_chat_model
import os
import dotenv
dotenv.load_dotenv()
SCOPE_MODEL_NAME = "gpt-4.1"
SCOPE_MODEL_PROVIDER = "openai"
SCOPE_MODEL_BASE_URL = "https://api.openai.com/v1"
SCOPE_MODEL_TEMPERATURE = 0.0
SCOPE_MODEL_PROVIDER_API_KEY = os.getenv("OPENAI_API_KEY")
scope_model = init_chat_model(
model=SCOPE_MODEL_NAME,
model_provider=SCOPE_MODEL_PROVIDER,
api_key=SCOPE_MODEL_PROVIDER_API_KEY,
base_url=SCOPE_MODEL_BASE_URL,
temperature=SCOPE_MODEL_TEMPERATURE
)
Copied

Funciones del agente Scopelink image 16

Como el agente va a tener que realizar dos acciones, pedir más información al usuario, si es preciso, y generar el research_brief, vamos a crear dos funciones para cada una de las acciones.

Empezamos con la función para pedir más información al usuario

  • Primero obtenemos el último mensaje del historial de mensajes, que será el mensaje del usuario
  • Luego hacemos una llamada al modelo forzando que devuelva una salida estructurada del tipo ClarifyWithUser
  • Si el LLM devuelve que se necesita más información, enviamos un mensaje al usuario indicando que se necesita más información y se termina el flujo del agente
  • Si el LLM devuelve que no se necesita más información, enviamos un mensaje al usuario indicando que se puede comenzar la investigación y en el flujo del agente se indica que se ejecute la función write_research_brief para generar el research_brief
	
< > Input
Python
from typing_extensions import Literal
from alive_progress import alive_bar
import traceback
import sys
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, get_buffer_string
from langgraph.graph import START, END
from langgraph.types import Command
def clarify_with_user(state: AgentState) -&gt; Command[Literal["write_research_brief", "__end__"]]:
"""
Determine if the user's request contains sufficient information to proceed with research.
Uses structured output to make deterministic decisions and avoid hallucination.
Routes to either research brief generation or ends with a clarification question.
"""
try:
print("⏳ Scope agent:")
format_messages([state.get("messages", [])[-1]], title="Real Human Message", msg_subtype='RealHumanMessage')
# Set up structured output model
structured_output_model = scope_model.with_structured_output(ClarifyWithUser)
# Invoke the model with clarification instructions
with alive_bar(monitor=False, stats=False, title="", spinner='dots_waves', bar='blocks') as bar:
response = structured_output_model.invoke([
HumanMessage(content=clarify_with_user_instructions.format(
messages=get_buffer_string(messages=state["messages"]),
date=get_today_str()
))
])
bar()
# Format and display the research messages
format_messages([response], title="Scope Assistant - need clarification?")
# Route based on clarification need
if response.need_clarification:
# Create a System message to show the decision
routing_message = SystemMessage(
content="Necesita aclaración por parte del usuario. Enviando pregunta aclaratoria..."
)
format_messages([routing_message], title="Scope System Message")
return Command(
goto=END,
update={"messages": [AIMessage(content=response.question)]}
)
else:
# Create a System message to show the decision
routing_message = SystemMessage(
content="No necesita aclaración por parte del usuario. Enviando mensaje de verificación..."
)
format_messages([routing_message], title="Scope System Message")
return Command(
goto="write_research_brief",
update={"messages": [AIMessage(content=response.verification)]}
)
except Exception as e:
# Get the traceback information
exc_type, exc_obj, exc_tb = sys.exc_info()
line_number = exc_tb.tb_lineno
# Print detailed error information
print(f" ❌ Error in clarify_with_user function:")
print(f" Line number: {line_number}")
print(f" Error type: {type(e).__name__}")
print(f" Error message: {str(e)}")
print(f" Full traceback:")
traceback.print_exc()
# Re-raise the exception to let the caller handle it
raise
Copied

Escribimos ahora la función para generar el research_brief

  • Primero obtenemos el último mensaje del historial de mensajes, que será
  • Luego hacemos una llamada al modelo forzando que devuelva una salida estructurada del tipo ResearchQuestion
  • Se termina el flujo del agente devolviendo el research_brief
	
< > Input
Python
def write_research_brief(state: AgentState):
"""
Transform the conversation history into a comprehensive research brief.
Uses structured output to ensure the brief follows the required format
and contains all necessary details for effective research.
"""
try:
# Set up structured output model
structured_output_model = scope_model.with_structured_output(ResearchQuestion)
print("⏳ Scope agent - Write research brief:")
format_messages([state.get("messages", [])[-1]], title="Scope Assistant - Write research brief")
# Generate research brief from conversation history
with alive_bar(monitor=False, stats=False, title="", spinner='dots_waves', bar='blocks') as bar:
response = structured_output_model.invoke([
HumanMessage(content=transform_messages_into_research_topic_prompt.format(
messages=get_buffer_string(state.get("messages", [])),
date=get_today_str()
))
])
bar()
# Format and display the research brief
format_messages([response], title="Scope Assistant - Research brief generated")
# Update state with generated research brief and pass it to the supervisor
return {
"research_brief": response.research_brief,
"supervisor_messages": [HumanMessage(content=f"{response.research_brief}.")]
}
except Exception as e:
# Get the traceback information
exc_type, exc_obj, exc_tb = sys.exc_info()
line_number = exc_tb.tb_lineno
# Print detailed error information
print(f" ❌ Error in write_research_brief function:")
print(f" Line number: {line_number}")
print(f" Error type: {type(e).__name__}")
print(f" Error message: {str(e)}")
print(f" Full traceback:")
traceback.print_exc()
# Re-raise the exception to let the caller handle it
raise
Copied

Grafo del agente Scopelink image 17

Ahora que tenemos todo lo necesario para construir el agente, vamos a definir el grafo del agente.

Primero creamos el agente con el estado AgentState y la entrada AgentInputState como hemos explicado antes, para que solo se pueda modificar el mensaje del estado interno del agente y no poder modificar el resto de variables del estado.

	
< > Input
Python
from langgraph.graph import StateGraph
# Build the scoping workflow
scope_graph_builder = StateGraph(AgentState, input_schema=AgentInputState)
Copied

Añadimos dos nodos, cada uno con las funciones que hemos definido antes

	
< > Input
Python
# Add workflow nodes
scope_graph_builder.add_node("clarify_with_user", clarify_with_user)
scope_graph_builder.add_node("write_research_brief", write_research_brief)
Copied
>_ Output
			
&lt;langgraph.graph.state.StateGraph at 0x10a3dc410&gt;

Y finalmente añadimos las aristas que conectan los nodos. Empezamos con el nodo START y lo conectamos con el nodo clarify_with_user. Por otro lado, unimos el nodo write_research_brief con el nodo END. Y es la función clarify_with_user la que se encarga de decidir si con el mensaje del usuario se pasa al nodo write_research_brief o se le pide más información al usuario y se pasa al nodo END.

	
< > Input
Python
from langgraph.graph import START, END
# Add workflow edges
scope_graph_builder.add_edge(START, "clarify_with_user")
scope_graph_builder.add_edge("write_research_brief", END)
Copied
>_ Output
			
&lt;langgraph.graph.state.StateGraph at 0x10a3dc410&gt;

Compilamos el grafo y representamos el diagrama de flujo

	
< > Input
Python
from langgraph.checkpoint.memory import InMemorySaver
from IPython.display import Image, display
checkpointer = InMemorySaver()
scope_agent = scope_graph_builder.compile(checkpointer=checkpointer)
# Visualize the workflow
display(Image(scope_agent.get_graph(xray=True).draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

Ejecución del agente Scopelink image 18

Creamos un hilo para poder hacer varias llamadas al agente, por si nos pide más información, y creamos el mensaje inicial del usuario con lo que queremos que cree el research_brief

	
< > Input
Python
thread = {"configurable": {"thread_id": "1"}}
user_message = "Quiero investigar las mejores cafeterías de Madrid."
result = await scope_agent.ainvoke({"messages": [HumanMessage(content=f"{user_message}.")]}, config=thread)
Copied
>_ Output
			
⏳ Scope agent:
>_ Output
			
╭───────────────────────────────────────────── 🧑 Real Human Message ─────────────────────────────────────────────╮
│ Quiero investigar las mejores cafeterías de Madrid.. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 2.7s
>_ Output
			
╭─────────────────────────────────── 🤖 Scope Assistant - need clarification? ────────────────────────────────────╮
│ need_clarification=True question='¿Podrías especificar qué criterios te interesan para considerar una cafetería │
│ como "mejor"? Por ejemplo: calidad del café, ambiente, ubicación, precio, opciones veganas, etc. ¿O prefieres │
│ una lista general?' verification='' │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭──────────────────────────────────────────── 🔧 Scope System Message ────────────────────────────────────────────╮
│ Necesita aclaración por parte del usuario. Enviando pregunta aclaratoria... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

Como se puede ver, el agente nos pide más información, por lo que volvemos a invocar al agente con la información que nos pide

	
< > Input
Python
user_message = "Examina la calidad del café para evaluar las mejores cafeterías de Madrid."
result = await scope_agent.ainvoke({"messages": [HumanMessage(content=f"{user_message}.")]}, config=thread)
Copied
>_ Output
			
⏳ Scope agent:
>_ Output
			
╭───────────────────────────────────────────── 🧑 Real Human Message ─────────────────────────────────────────────╮
│ Examina la calidad del café para evaluar las mejores cafeterías de Madrid.. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 2.3s
>_ Output
			
╭─────────────────────────────────── 🤖 Scope Assistant - need clarification? ────────────────────────────────────╮
│ need_clarification=False question='' verification='Gracias por la información. Entiendo que deseas un informe │
│ sobre las mejores cafeterías de Madrid, evaluadas específicamente por la calidad del café. Comenzaré la │
│ investigación basándome en este criterio principal. Ahora iniciaré el proceso de investigación.' │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭──────────────────────────────────────────── 🔧 Scope System Message ────────────────────────────────────────────╮
│ No necesita aclaración por parte del usuario. Enviando mensaje de verificación... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Scope agent - Write research brief:
>_ Output
			
╭─────────────────────────────────── 🤖 Scope Assistant - Write research brief ───────────────────────────────────╮
│ Gracias por la información. Entiendo que deseas un informe sobre las mejores cafeterías de Madrid, evaluadas │
│ específicamente por la calidad del café. Comenzaré la investigación basándome en este criterio principal. Ahora │
│ iniciaré el proceso de investigación. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 3.9s
>_ Output
			
╭───────────────────────────────── 🤖 Scope Assistant - Research brief generated ─────────────────────────────────╮
│ research_brief='Quiero investigar cuáles son las mejores cafeterías de Madrid evaluadas específicamente por la │
│ calidad del café. Para ello, me interesa identificar cafeterías que destaquen por la excelencia en el sabor, │
│ aroma, frescura y preparación del café, considerando factores como el origen de los granos, el método de │
│ tostado y las técnicas de extracción utilizadas. No tengo preferencias sobre el rango de precios, ubicación, │
│ ambiente, opciones alimenticias o servicios adicionales, por lo que estos aspectos pueden considerarse abiertos │
│ o secundarios. Solicito priorizar fuentes primarias y confiables, como los sitios web oficiales de las │
│ cafeterías, reseñas de expertos en café y plataformas reconocidas de opinión de usuarios en español.' │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

Con la nueva información aclaratoria, el agente ha generado el research_brief, que será lo que enviaremos al agente de investigación.

Researcherlink image 19

Si recordamos cómo iba a ser la arquitectura del deep researcher, vemos que hay un Research supervisor que se encarga de asignar investigaciones a diferentes Researchers

deep researcher architecture

Así que ahora vamos a definir el Researcher que se encargará de realizar la investigación.

Researcher promptslink image 20

Primero definimos el prompt del Researcher, se le indica

  • Qué es un investigador con acceso a herramientas
  • Se le dice que las dos herramientas que tiene son las de búsqueda web y la de reflexión
  • Se le dan unas instrucciones de cómo debe realizar la investigación
  • Se le indica que debe utilizar la herramienta de reflexión después de cada búsqueda
  • Se le da un presupuesto de llamadas a herramientas
	
< > Input
Python
research_agent_prompt = """Eres un asistente de investigación que lleva a cabo una investigación sobre el tema introducido por el usuario. Para contextualizar, hoy es {date}.
&lt;Task&gt;
Tu trabajo consiste en utilizar herramientas para recopilar información sobre el tema introducido por el usuario.
Puedes utilizar cualquiera de las herramientas que se te proporcionan para encontrar recursos que te ayuden a responder a la pregunta de investigación. Puedes utilizar estas herramientas en serie o en paralelo, tu investigación se lleva a cabo en un bucle de llamada de herramientas.
&lt;/Task&gt;
&lt;Available Tools&gt;
Tienes acceso a dos herramientas principales:
1. **tavily_search**: para realizar búsquedas en la web con el fin de recopilar información.
2. **think_tool**: para la reflexión y la planificación estratégica durante la investigación.
**IMPORTANTE: utiliza think_tool después de cada búsqueda para reflexionar sobre los resultados y planificar los siguientes pasos**.
**CRÍTICO: Siempre debes proporcionar el parámetro 'reflection' con contenido detallado cuando llames a think_tool. Nunca llames a esta herramienta sin argumentos**.
&lt;/Available Tools&gt;
&lt;Instructions&gt;
Piensa como un investigador humano con tiempo limitado. Sigue estos pasos:
1. **Lee la pregunta con atención**: ¿qué información específica necesita el usuario?
2. **Empieza con búsquedas más amplias**: utiliza primero consultas amplias y exhaustivas.
3. **Después de cada búsqueda, haz una pausa y evalúa**: ¿tengo suficiente para responder? ¿Qué me falta?
4. **Realiza búsquedas más específicas a medida que recopilas información**: completa los huecos.
5. **Detente cuando puedas responder con seguridad**: no sigas buscando la perfección.
&lt;/Instructions&gt;
&lt;Hard Limits&gt;
**Presupuestos de llamadas a herramientas** (evita búsquedas excesivas):
- **Consultas simples**: utiliza un máximo de 2-3 llamadas a herramientas de búsqueda.
- **Consultas complejas**: utiliza un máximo de 5 llamadas a herramientas de búsqueda
- **Detente siempre**: después de 5 llamadas a herramientas de búsqueda si no puedes encontrar las fuentes adecuadas
**Detente inmediatamente cuando**:
- Puedas responder a la pregunta del usuario de forma exhaustiva
- Tengas más de 3 ejemplos/fuentes relevantes para la pregunta
- Tus últimas 2 búsquedas hayan devuelto información similar
&lt;/Hard Limits&gt;
&lt;Show Your Thinking&gt;
Después de cada llamada a la herramienta de búsqueda, utiliza think_tool para analizar los resultados:
- ¿Qué información clave he encontrado?
- ¿Qué falta?
- ¿Tengo suficiente para responder a la pregunta de forma exhaustiva?
- ¿Debo seguir buscando o dar mi respuesta?
&lt;/Show Your Thinking&gt;
"""
Copied

Ahora definimos el prompt de un LLM que resume contenido de una página web.

  • Se le da el contenido de una página web
  • Se le dan unas instrucciones de cómo debe realizar el resumen
  • Se le da un formato en el que debe devolver el resumen
  • Se le da la fecha actual
	
< > Input
Python
summarize_webpage_prompt = """Tu tarea consiste en resumir el contenido sin procesar de una página web obtenida mediante una búsqueda en Internet. Tu objetivo es crear un resumen que conserve la información más importante de la página web original. Este resumen será utilizado por un agente de investigación posterior, por lo que es fundamental mantener los detalles clave sin perder información esencial.
Este es el contenido sin procesar de la página web:
&lt;webpage_content&gt;
{webpage_content}
&lt;/webpage_content&gt;
Sigue estas pautas para crear tu resumen:
1. Identifica y conserva el tema principal o el propósito de la página web.
2. Conserva los datos, estadísticas y puntos clave que son fundamentales para el mensaje del contenido.
3. Conserva las citas importantes de fuentes o expertos fiables.
4. Mantén el orden cronológico de los acontecimientos si el contenido es sensible al tiempo o histórico.
5. Conserva las listas o instrucciones paso a paso, si las hay.
6. Incluye fechas, nombres y lugares relevantes que sean cruciales para comprender el contenido.
7. Resume las explicaciones largas manteniendo intacto el mensaje principal.
Cuando se trata de diferentes tipos de contenido:
- Para artículos de noticias: concéntrate en quién, qué, cuándo, dónde, por qué y cómo.
- Para contenido científico: conserva la metodología, los resultados y las conclusiones.
- Para artículos de opinión: mantén los argumentos principales y los puntos de apoyo.
- Para páginas de productos: mantén las características clave, las especificaciones y los puntos de venta únicos.
Tu resumen debe ser significativamente más corto que el contenido original, pero lo suficientemente completo como para constituir una fuente de información por tí mismo. Intenta que tenga entre un 25 % y un 30 % de la longitud original, a menos que el contenido ya sea conciso.
Presenta tu resumen en el siguiente formato:
```
{{
"summary": "Tu resumen aquí, estructurado con párrafos o viñetas adecuados según sea necesario",
"key_excerpts": "Primera cita o extracto importante, Segunda cita o extracto importante, Tercer cita o extracto importante, ...Añade más extractos según sea necesario, hasta un máximo de 5"
}}
```
Aquí hay dos ejemplos de buenos resúmenes:
Ejemplo 1 (para un artículo de noticias):
```json
{{
"summary": "El 15 de julio de 2023, la NASA lanzó con éxito la misión Artemis II desde el Centro Espacial Kennedy. Se trata de la primera misión tripulada a la Luna desde el Apolo 17 en 1972. La tripulación de cuatro personas, dirigida por la comandante Jane Smith, orbitará la Luna durante 10 días antes de regresar a la Tierra. Esta misión es un paso crucial en los planes de la NASA para establecer una presencia humana permanente en la Luna para 2030.",
"key_excerpts": "Artemis II representa una nueva era en la exploración espacial, dijo el administrador de la NASA, John Doe. La misión pondrá a prueba sistemas críticos para futuras estancias de larga duración en la Luna, explicó la ingeniera jefe Sarah Johnson. No solo vamos a volver a la Luna, vamos a avanzar hacia la Luna, declaró la comandante Jane Smith durante la rueda de prensa previa al lanzamiento"
}}
```
Ejemplo 2 (para un artículo científico):
```json
{{
"summary": "Un nuevo estudio publicado en Nature Climate Change revela que el nivel del mar está subiendo más rápido de lo que se pensaba. Los investigadores analizaron datos satelitales de 1993 a 2022 y descubrieron que la tasa de aumento del nivel del mar se ha acelerado en 0,08 mm/año² durante las últimas tres décadas. Esta aceleración se atribuye principalmente al derretimiento de los casquetes polares de Groenlandia y la Antártida. El estudio prevé que, si las tendencias actuales continúan, el nivel global del mar podría aumentar hasta 2 metros para 2100, lo que supondría un riesgo significativo para las comunidades costeras de todo el mundo.",
"key_excerpts": "Nuestros hallazgos indican una clara aceleración en el aumento del nivel del mar, lo que tiene importantes implicaciones para la planificación costera y las estrategias de adaptación», afirmó la autora principal, la Dra. Emily Brown. La velocidad de deshielo de las capas de hielo de Groenlandia y la Antártida se ha triplicado desde la década de 1990, según el estudio. Sin una reducción inmediata y sustancial de las emisiones de gases de efecto invernadero, nos enfrentamos a un aumento potencialmente catastrófico del nivel del mar a finales de este siglo, advirtió el coautor, el profesor Michael Green."
}}
```
Recuerda que tu objetivo es crear un resumen que pueda ser fácilmente comprendido y utilizado por un agente de investigación posterior, conservando al mismo tiempo la información más importante de la página web original.
La fecha de hoy es {date}.
"""
Copied

Una vez que se ha resumido el contenido de la página web, se llama a otro LLM para que limpie los resultados de la investigación y del razonamiento interno a la hora de resumir el contenido.

  • Se le da contenido de la investigación
  • Se le dan unas instrucciones de cómo debe realizar el resumen
  • Se le da un formato en el que debe de devolver el resumen
  • Se le da la fecha actual
  • Se le indica que debe conservar todas las declaraciones e información relevante recopilada por el investigador
  • Se le indica que debe repetir la información clave textualmente

Este prompt es importante, porque se le pide que cree una lista numerada con las fuentes utilizadas. Esto es útil para que en siguientes investigaciones no se vuelvan a utilizar las mismas fuentes. Pero, sobre todo, para que en el informe final al usuario, se le pueda mostrar las fuentes utilizadas para cada afirmación.

	
< > Input
Python
compress_research_system_prompt = """Eres un asistente de investigación que ha investigado un tema utilizando diversas herramientas y búsquedas en Internet. Tu trabajo ahora consiste en limpiar los resultados, conservando todas las declaraciones e información relevantes recopiladas por el investigador. Para contextualizar, hoy es {date}.
&lt;Task&gt;
Debes limpiar la información recopilada de las herramientas y búsquedas en Internet en los mensajes existentes.
Toda la información relevante debe repetirse y reescribirse textualmente, pero en un formato más limpio.
El objetivo de este paso es simplemente eliminar cualquier información obviamente irrelevante o duplicada.
Por ejemplo, si tres fuentes dicen «X», podrías decir «Estas tres fuentes afirman X».
Solo estos resultados completos y limpios se devolverán al usuario, por lo que es esencial que no pierdas ninguna información de los mensajes sin procesar.
&lt;/Task&gt;
&lt;Tool Call Filtering&gt;
**IMPORTANTE**: Al procesar los mensajes de investigación, céntrate solo en el contenido sustantivo de la investigación:
- **Incluir**: Todos los resultados de tavily_search y los hallazgos de la búsqueda web
- **Excluir**: las llamadas y respuestas de think_tool, ya que son reflexiones internas del agente para la toma de decisiones y no deben incluirse en el informe final de la investigación
- **Céntrate en**: la información real recopilada de fuentes externas, no en el proceso de razonamiento interno del agente.
Las llamadas a think_tool contienen reflexiones estratégicas y notas sobre la toma de decisiones que son internas al proceso de investigación, pero no contienen información factual que deba conservarse en el informe final.
&lt;/Tool Call Filtering&gt;
&lt;Guidelines&gt;
1. Los resultados deben ser exhaustivos e incluir TODA la información y las fuentes que el investigador haya recopilado a partir de las llamadas a herramientas y las búsquedas en la web. Se espera que repita la información clave textualmente.
2. Este informe puede tener la extensión necesaria para incluir TODA la información que el investigador haya recopilado.
3. En tu informe, debes incluir citas en línea para cada fuente que el investigador haya encontrado.
4. Debes incluir una sección «Fuentes» al final del informe en la que se enumeran todas las fuentes que el investigador ha encontrado con las citas correspondientes, referenciadas en relación con las afirmaciones del informe.
5. Asegúrese de incluir TODAS las fuentes recopiladas por el investigador en el informe y cómo se utilizaron para responder a la pregunta.
6. Es muy importante no perder ninguna fuente. Más adelante se utilizará un LLM para fusionar este informe con otros, por lo que es esencial disponer de todas las fuentes.
&lt;/Guidelines&gt;
&lt;Output Format&gt;
El informe debe estructurarse de la siguiente manera:
**Lista de consultas y llamadas a herramientas realizadas**
**Resultados completos**
**Lista de todas las fuentes relevantes (con citas en el informe)**
&lt;/Output Format&gt;
&lt;Citation Rules&gt;
- Asigna a cada URL única un número de citación único en tu texto.
- Termina con ### Fuentes, enumerando cada fuente con los números correspondientes.
- IMPORTANTE: Numera las fuentes de forma secuencial sin saltos (1, 2, 3, 4...) en la lista final, independientemente de las fuentes que elijas.
- Ejemplo de formato:
[1] Título de la fuente: URL
[2] Título de la fuente: URL
&lt;/Citation Rules&gt;
Recordatorio importante: Es extremadamente importante que cualquier información que sea remotamente relevante para el tema de investigación del usuario se conserve textualmente (es decir, no la reescribas, no la resumas, no la parafrasees).
"""
Copied

Por último, definimos el prompt de una LLM que se encarga de depurar los resultados de la investigación para la pregunta particular que se le ha asignado.

  • Se le da el tema de investigación
  • Se le dan unas instrucciones de cómo debe realizar el resumen
	
< > Input
Python
compress_research_human_message = """Todos los mensajes anteriores se refieren a la investigación realizada por un investigador de IA sobre el siguiente tema de investigación:
TEMA DE INVESTIGACIÓN: {research_topic}
Tu tarea consiste en depurar los resultados de esta investigación conservando TODA la información relevante para responder a esta pregunta de investigación específica.
REQUISITOS FUNDAMENTALES:
- NO resumas ni parafrasees la información: consérvala textualmente.
- NO pierdas ningún detalle, dato, nombre, número o hallazgo específico
- NO filtres la información que parezca relevante para el tema de investigación
- Organiza la información en un formato más claro, pero mantén todo el contenido
- Incluye TODAS las fuentes y citas encontradas durante la investigación
- Recuerda que esta investigación se llevó a cabo para responder a la pregunta específica anterior
Los resultados depurados se utilizarán para la elaboración del informe final, por lo que es fundamental que sean exhaustivos."""
Copied

Researcher statelink image 21

Ahora tenemos algo similar que con el estado de Scope, tenemos dos estados ResearcherState y ResearcherInputState, vamos a verlos y luego lo explicamos

	
< > Input
Python
from typing_extensions import TypedDict, Annotated, List, Sequence
class ResearcherState(TypedDict):
"""
State for the research agent containing message history and research metadata.
This state tracks the researcher's conversation, iteration count for limiting
tool calls, the research topic being investigated, compressed findings,
and raw research notes for detailed analysis.
"""
researcher_messages: Annotated[Sequence[BaseMessage], add_messages]
tool_call_iterations: int
research_topic: str
compressed_research: str
raw_notes: Annotated[List[str], operator.add]
class ResearcherOutputState(TypedDict):
"""
Output state for the research agent containing final research results.
This represents the final output of the research process with compressed
research findings and all raw notes from the research process.
"""
compressed_research: str
raw_notes: Annotated[List[str], operator.add]
researcher_messages: Annotated[Sequence[BaseMessage], add_messages]
Copied

Se puede ver que todas las variables de ResearcherOutputState (compressed_research, raw_notes y researcher_messages) están presentes en ResearcherState, pero las variables tool_call_iterations y research_topic de ResearcherState no están en ResearcherOutputState.

Esto es porque son variables que el agente de investigación necesita, pero no son relevantes para el usuario final, que en este caso va a ser el Supervisor.

Salida estructurada del Researcherlink image 22

Vamos a definir cómo queremos que sea la salida del Researcher.

	
< > Input
Python
class Summary(BaseModel):
"""Schema for webpage content summarization."""
summary: str = Field(description="Concise summary of the webpage content")
key_excerpts: str = Field(description="Important quotes and excerpts from the content")
Copied

Vemos que queremos un resumen y las citas clave de la investigación.

Tools del Researcherlink image 23

Cuando explicamos el agente Scope, explicamos las funciones del agente, mientras que ahora vamos a definir las tools, ¿cuál es la diferencia?

Las funciones las hemos implementado nosotros y se van a ejecutar siempre que el código vaya por un flujo que hemos definido. Sin embargo, las tools son herramientas que tiene el agente y que es él quien decide si utilizarlas o no, y cuándo utilizarlas.

Tool de búsqueda weblink image 24

Para buscar por internet, vamos a utilizar la herramienta tavily_search que ya hemos importado antes.

Antes de definir la tool, primero definimos algunas funciones auxiliares que nos van a ayudar a definir la tool.

Primero definimos una función para realizar varias búsquedas en paralelo.

	
< > Input
Python
def tavily_search_multiple(
search_queries: List[str],
max_results: int = 3,
topic: Literal["general", "news", "finance"] = "general",
include_raw_content: bool = True,
) -&gt; List[dict]:
"""Perform search using Tavily API for multiple queries.
Args:
search_queries: List of search queries to execute
max_results: Maximum number of results per query
topic: Topic filter for search results
include_raw_content: Whether to include raw webpage content
Returns:
List of search result dictionaries
"""
# Execute searches sequentially. Note: yon can use AsyncTavilyClient to parallelize this step.
search_docs = []
for query in search_queries:
result = tavily_client.search(
query,
max_results=max_results,
include_raw_content=include_raw_content,
topic=topic
)
search_docs.append(result)
return search_docs
Copied

Como hacemos varias búsquedas en paralelo, puede que esas búsquedas hayan encontrado datos en las mismas fuentes, por lo que descartamos las búsquedas duplicadas con una función

	
< > Input
Python
def deduplicate_search_results(search_results: List[dict]) -&gt; dict:
"""Deduplicate search results by URL to avoid processing duplicate content.
Args:
search_results: List of search result dictionaries
Returns:
Dictionary mapping URLs to unique results
"""
unique_results = {}
for response in search_results:
for result in response['results']:
url = result['url']
if url not in unique_results:
unique_results[url] = result
return unique_results
Copied

Ahora definimos una función para resumir el contenido de una página web, para ello vamos a usar el prompt summarize_webpage_prompt

	
< > Input
Python
def summarize_webpage_content(webpage_content: str) -&gt; str:
"""Summarize webpage content using the configured summarization model.
Args:
webpage_content: Raw webpage content to summarize
Returns:
Formatted summary with key excerpts
"""
try:
# Set up structured output model for summarization
structured_model = summarization_model.with_structured_output(Summary)
# Generate summary
summary = structured_model.invoke([
HumanMessage(content=summarize_webpage_prompt.format(
webpage_content=webpage_content,
date=get_today_str()
))
])
# Format summary with clear structure
formatted_summary = (
f"&lt;summary&gt; {summary.summary} &lt;/summary&gt; "
f"&lt;key_excerpts&gt; {summary.key_excerpts} &lt;/key_excerpts&gt;"
)
return formatted_summary
except Exception as e:
print(f"Failed to summarize webpage: {str(e)}")
return webpage_content[:1000] + "..." if len(webpage_content) &gt; 1000 else webpage_content
Copied

Con el contenido de la búsqueda resumido, creamos una función para limpiar los resultados de la investigación.

	
< > Input
Python
def process_search_results(unique_results: dict) -&gt; dict:
"""Process search results by summarizing content where available.
Args:
unique_results: Dictionary of unique search results
Returns:
Dictionary of processed results with summaries
"""
summarized_results = {}
for url, result in unique_results.items():
# Use existing content if no raw content for summarization
if not result.get("raw_content"):
content = result['content']
else:
# Summarize raw content for better processing
content = summarize_webpage_content(result['raw_content'])
summarized_results[url] = {
'title': result['title'],
'content': content
}
return summarized_results
Copied

Y por último, creamos una función para darle un formato concreto al contenido de la investigación.

	
< > Input
Python
def format_search_output(summarized_results: dict) -&gt; str:
"""Format search results into a well-structured string output.
Args:
summarized_results: Dictionary of processed search results
Returns:
Formatted string of search results with clear source separation
"""
if not summarized_results:
return "No valid search results found. Please try different search queries or use a different search API."
formatted_output = "Search results: "
for i, (url, result) in enumerate(summarized_results.items(), 1):
formatted_output += f" --- SOURCE {i}: {result['title']} --- "
formatted_output += f"URL: {url} "
formatted_output += f"SUMMARY: {result['content']} "
formatted_output += "-" * 80 + " "
return formatted_output
Copied

Con todas estas funciones auxiliares, ya podemos definir la tool que va a usar las funciones que hemos definido. Primero realiza varias búsquedas en paralelo, descarta los resultados duplicados, limpia los resultados y da un formato concreto al contenido de la investigación.

	
< > Input
Python
from langchain_core.tools import tool, InjectedToolArg
@tool(parse_docstring=True)
def tavily_search(
query: str,
max_results: Annotated[int, InjectedToolArg] = 3,
topic: Annotated[Literal["general", "news", "finance"], InjectedToolArg] = "general",
) -&gt; str:
"""Fetch results from Tavily search API with content summarization.
Args:
query: A single search query to execute
max_results: Maximum number of results to return
topic: Topic to filter results by ('general', 'news', 'finance')
"""
# Execute search for single query
search_results = tavily_search_multiple(
[query], # Convert single query to list for the internal function
max_results=max_results,
topic=topic,
include_raw_content=True,
)
# Deduplicate results by URL to avoid processing duplicate content
unique_results = deduplicate_search_results(search_results)
# Process results with summarization
summarized_results = process_search_results(unique_results)
# Format output for consumption
return format_search_output(summarized_results)
Copied

Se puede ver que hemos usado parse_docstring=True al definir la tool. Esto hace que el docstring de la función se le pase al LLM para que tenga esa información como contexto. Si no hiciéramos eso, el LLM solo recibiría como contexto

{
"query": str,
"max_results": int,
"topic": Literal["general", "news", "finance"]
}

Es decir, solo sabría qué tipo de argumentos espera la función, pero no sabría para qué se usa. Pero haciendo parse_docstring=True, lo que el LLM recibe es algo así:

Tengo un buscador que:
- Recibe:
- query (string): "Una consulta de búsqueda simple"
- max_results (int): "Número máximo de resultados a devolver"
- topic (Literal): "Categoría para filtrar ('general', 'news', 'finance')"

Aunque hay otra cosa que hemos hecho que hace que no sea realmente así, las líneas

  max_results: Annotated[int, InjectedToolArg] = 3,
topic: Annotated[Literal["general", "news", "finance"], InjectedToolArg] = "general",

Usan InjectedToolArg que hace que el valor de esos argumentos estén fijos y no los decida el LLM. Imagina que el LLM decide que el número máximo de resultados sea 100, podría hacer que la búsqueda sea demasiado larga.

Por lo que, debido a esto, lo que el LLM realmente recibe es algo así:

Tengo un buscador que:
- Recibe:
- query (string): "Una consulta de búsqueda simple"

Tool de reflexiónlink image 25

Como queremos que el agente reflexione después de cada búsqueda, le damos una tool para ello

	
< > Input
Python
@tool(parse_docstring=True)
def think_tool(reflection: str = "") -&gt; str:
"""Record your strategic reflection on research progress (REQUIRED).
You MUST provide a detailed reflection analyzing your current findings,
gaps, and next steps. Never call this tool without providing reflection text.
Use this tool after each search to analyze results and plan next steps systematically.
This creates a deliberate pause in the research workflow for quality decision-making.
When to use:
- After receiving search results: What key information did I find?
- Before deciding next steps: Do I have enough to answer comprehensively?
- When assessing research gaps: What specific information am I still missing?
- Before concluding research: Can I provide a complete answer now?
Args:
reflection: (REQUIRED) Your detailed reflection on research progress, findings, gaps, and next steps.
Your reflection should include analysis of current findings, gap assessment,
quality evaluation, and strategic decision about next steps.
"""
if not reflection:
return "Reflection recorded: [No reflection provided]"
return f"Reflection recorded: {reflection}"
Copied

Al igual que antes, usamos parse_docstring=True para que el docstring se le pase al LLM como contexto.

Modelos de lenguaje del researcherlink image 26

Ahora que hemos definido las tools, vamos a definir los diferentes modelos de lenguaje que vamos a usar, ya que algunos de ellos van a usar las tools

Modelo de lenguaje de resumen del researcherlink image 27

Primero vamos a crear el modelo de lenguaje que vamos a usar en la función summarize_webpage_content que hemos definido antes

	
< > Input
Python
from langchain.chat_models import init_chat_model
import os
import dotenv
dotenv.load_dotenv()
SUMMARIZATION_MODEL_NAME = "gpt-4.1-mini"
SUMMARIZATION_MODEL_PROVIDER = "openai"
SUMMARIZATION_MODEL_BASE_URL = "https://api.openai.com/v1"
SUMMARIZATION_MODEL_TEMPERATURE = 0.0
SUMMARIZATION_MODEL_PROVIDER_API_KEY = os.getenv("OPENAI_API_KEY")
summarization_model = init_chat_model(
model=SUMMARIZATION_MODEL_NAME,
model_provider=SUMMARIZATION_MODEL_PROVIDER,
api_key=SUMMARIZATION_MODEL_PROVIDER_API_KEY,
base_url=SUMMARIZATION_MODEL_BASE_URL,
temperature=SUMMARIZATION_MODEL_TEMPERATURE
)
Copied

Modelo de lenguaje de investigaciónlink image 28

Ahora vamos a definir el modelo de lenguaje que se va a encargar de la investigación

	
< > Input
Python
RESEARCH_MODEL_NAME = "anthropic:claude-sonnet-4-5-20250929"
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
RESEARCH_MODEL_MAX_TOKENS = 4096
research_model = init_chat_model(
model=RESEARCH_MODEL_NAME,
max_tokens=RESEARCH_MODEL_MAX_TOKENS
)
Copied

Ahora le damos al modelo de lenguaje de investigación acceso a las tools que hemos definido

	
< > Input
Python
researcher_tools = [tavily_search, think_tool]
model_with_tools = research_model.bind_tools(researcher_tools)
Copied

Modelo de lenguaje de compresión de la informaciónlink image 29

Por último, definimos un modelo de lenguaje que se va a encargar de comprimir la información de la investigación

	
< > Input
Python
COMPRESS_MODEL_NAME = "gpt-4.1"
COMPRESS_MODEL_PROVIDER = "openai"
COMPRESS_MODEL_BASE_URL = "https://api.openai.com/v1"
COMPRESS_MODEL_TEMPERATURE = 0.0
COMPRESS_MODEL_PROVIDER_API_KEY = os.getenv("OPENAI_API_KEY")
compress_model = init_chat_model(
model=COMPRESS_MODEL_NAME,
model_provider=COMPRESS_MODEL_PROVIDER,
api_key=COMPRESS_MODEL_PROVIDER_API_KEY,
base_url=COMPRESS_MODEL_BASE_URL,
temperature=COMPRESS_MODEL_TEMPERATURE,
max_tokens=32000
)
Copied

Funciones del agentelink image 30

Como hemos visto antes, por un lado están las tools del agente, que son herramientas que el LLM va a elegir cuándo y cómo usar, y por otro lado están las funciones del agente, que son las funciones que se van a ejecutar en los nodos del grafo, y se van a ejecutar en el momento en que nosotros lo definimos por código.

Primero creamos una función que se va a encargar de llamar al modelo de lenguaje de investigación con las herramientas a las que tiene acceso. El modelo decidirá si tiene que usar una tool o no, y en caso de que lo haga, se le pasará el control a la tool para que se ejecute.

	
< > Input
Python
def llm_call(state: ResearcherState):
"""Analyze current state and decide on next actions.
The model analyzes the current conversation state and decides whether to:
1. Call search tools to gather more information
2. Provide a final answer based on gathered information
Returns updated state with the model's response.
"""
try:
# Show progress bar while waiting for LLM response
print("⏳ Researcher agent:")
if isinstance(state.get('researcher_messages', [])[-1], HumanMessage):
format_messages([state.get("researcher_messages", [])[-1]], title="Researcher Agent - Simulated Human Message")
with alive_bar(monitor=False, stats=False, title="", spinner='dots_waves', bar='blocks') as bar:
research_messages = model_with_tools.invoke(
[SystemMessage(content=research_agent_prompt)] + state["researcher_messages"]
)
bar() # Complete the progress bar
# Format and display the research messages
format_messages([research_messages], title="Researcher Agent response")
# Return the research messages
return {
"researcher_messages": [
research_messages
]
}
except Exception as e:
# Get the traceback information
exc_type, exc_obj, exc_tb = sys.exc_info()
line_number = exc_tb.tb_lineno
# Print detailed error information
print(f" ❌ Error in llm_call function:")
print(f" Line number: {line_number}")
print(f" Error type: {type(e).__name__}")
print(f" Error message: {str(e)}")
print(f" Full traceback:")
traceback.print_exc()
# Re-raise the exception to let the caller handle it
raise
Copied

Si el modelo de lenguaje de investigación decide que no tiene que usar una tool, se le pasa el control a la función tool_node para que se ejecute la tool que el LLM ha elegido.

	
< > Input
Python
from langchain_core.messages import ToolMessage, ToolCall
tools_by_name = {tool.name: tool for tool in researcher_tools}
def tool_node(state: ResearcherState):
"""Execute all tool calls from the previous LLM response.
Executes all tool calls from the previous LLM responses.
Returns updated state with tool execution results.
"""
try:
# Get the tool calls
tool_calls = state["researcher_messages"][-1].tool_calls
# Format the tool calls
format_messages([ToolCall(
name="Tool Calls",
args=tool_calls,
id="tool_call_id"
)], title="Researcher Agent - Tool calls")
# Execute all tool calls
observations = []
for tool_call in tool_calls:
with alive_bar(monitor=False, stats=False, title=f"", spinner='dots_waves', bar='blocks') as bar:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
observations.append(observation)
bar()
# Format and display the result immediately
format_messages([ToolMessage(
content=observation,
name=tool_call["name"],
tool_call_id=tool_call["id"]
)], title="Researcher Agent - Tool calls result")
# Create tool message outputs
tool_outputs = [
ToolMessage(
content=observation,
name=tool_call["name"],
tool_call_id=tool_call["id"]
) for observation, tool_call in zip(observations, tool_calls)
]
# Return the tool outputs
return {"researcher_messages": tool_outputs}
except Exception as e:
# Get the traceback information
exc_type, exc_obj, exc_tb = sys.exc_info()
line_number = exc_tb.tb_lineno
# Print detailed error information
print(f" ❌ Error in tool_node function:")
print(f" Line number: {line_number}")
print(f" Error type: {type(e).__name__}")
print(f" Error message: {str(e)}")
print(f" Full traceback:")
traceback.print_exc()
# Re-raise the exception to let the caller handle it
raise
Copied

Ahora vamos a definir la función que se va a encargar de comprimir la información de la investigación.

	
< > Input
Python
from langchain_core.messages import SystemMessage, HumanMessage, filter_messages
def compress_research(state: ResearcherState) -&gt; dict:
"""Compress research findings into a concise summary.
Takes all the research messages and tool outputs and creates
a compressed summary suitable for the supervisor's decision-making.
"""
try:
system_message = compress_research_system_prompt.format(date=get_today_str())
messages = [SystemMessage(content=system_message)] + state.get("researcher_messages", []) + [HumanMessage(content=compress_research_human_message)]
print("⏳ Compressing Research:")
with alive_bar(monitor=False, stats=False, title="", spinner='dots_waves', bar='blocks') as bar:
response = compress_model.invoke(messages)
bar()
# Format and display the compressed research
format_messages([response], title="Researcher Agent - Compressed Research")
# Extract raw notes from tool and AI messages
raw_notes = [
str(m.content) for m in filter_messages(
state["researcher_messages"],
include_types=["tool", "ai"]
)
]
# Return the compressed research and raw notes
return {
"compressed_research": str(response.content),
"raw_notes": [" ".join(raw_notes)]
}
except Exception as e:
# Get the traceback information
exc_type, exc_obj, exc_tb = sys.exc_info()
line_number = exc_tb.tb_lineno
# Print detailed error information
print(f" ❌ Error in compress_research function:")
print(f" Line number: {line_number}")
print(f" Error type: {type(e).__name__}")
print(f" Error message: {str(e)}")
print(f" Full traceback:")
traceback.print_exc()
# Re-raise the exception to let the caller handle it
raise
Copied

Por último, vamos a definir la función que se va a encargar de decidir si se continúa con la investigación o se finaliza.

Lo que va a hacer es comprobar si el último mensaje del modelo de lenguaje de investigación contiene tool calls, y en caso de que lo haga, quiere decir que el LLM ha decidido usar una tool, por lo que se le pasa el control a la función tool_node para que se ejecute la tool que el LLM ha elegido.

En caso contrario, si el último mensaje no contiene tool calls, quiere decir que el LLM ha decidido que ya tiene suficiente información y se le pasa el control a la función compress_research para que se comprima la información de la investigación.

	
< > Input
Python
def should_continue(state: ResearcherState) -&gt; Literal["tool_node", "compress_research"]:
"""Determine whether to continue research or provide final answer.
Determines whether the agent should continue the research loop or provide
a final answer based on whether the LLM made tool calls.
Returns:
"tool_node": Continue to tool execution
"compress_research": Stop and compress research
"""
try:
messages = state["researcher_messages"]
last_message = messages[-1]
# If the LLM makes a tool call, continue to tool execution
if last_message.tool_calls:
# Create a System message to show the decision
decision_message = SystemMessage(
content="Last message contains tool calls. Continuing to tool execution..."
)
format_messages([decision_message], title="Researcher Agent - Should continue")
return "tool_node"
# Otherwise, we have a final answer
else:
# Create a System message to show the decision
decision_message = SystemMessage(
content="No tool calls found. Stopping research and compressing findings..."
)
format_messages([decision_message], title="Researcher Agent - Should compress research")
return "compress_research"
except Exception as e:
# Get the traceback information
exc_type, exc_obj, exc_tb = sys.exc_info()
line_number = exc_tb.tb_lineno
# Print detailed error information
print(f" ❌ Error in should_continue function:")
print(f" Line number: {line_number}")
print(f" Error type: {type(e).__name__}")
print(f" Error message: {str(e)}")
print(f" Full traceback:")
traceback.print_exc()
# Re-raise the exception to let the caller handle it
raise
Copied

Grafo del researcherlink image 31

Ya tenemos todo lo necesario para construir el grafo del agente researcher.

Primero creamos el grafo

	
< > Input
Python
# Build the agent workflow
researcher_agent_builder = StateGraph(ResearcherState, output_schema=ResearcherOutputState)
Copied

Ahora creamos los nodos que van a componer el grafo. Van a utilizar las funciones que hemos definido antes.

	
< > Input
Python
# Add nodes to the graph
researcher_agent_builder.add_node("llm_call", llm_call)
researcher_agent_builder.add_node("tool_node", tool_node)
researcher_agent_builder.add_node("compress_research", compress_research)
Copied
>_ Output
			
&lt;langgraph.graph.state.StateGraph at 0x1112bddf0&gt;

Ya tenemos los nodos, los unimos con las aristas.

  • Del nodo START se pasa al nodo llm_call
  • Del nodo llm_call se pasa al nodo tool_node si el último mensaje contiene tool calls, o al nodo compress_research si no contiene tool calls
  • Del nodo tool_node se pasa al nodo llm_call para que el LLM pueda decidir si sigue investigando o no
  • Del nodo compress_research se pasa al nodo END
	
< > Input
Python
# Add edges to connect nodes
researcher_agent_builder.add_edge(START, "llm_call")
researcher_agent_builder.add_conditional_edges(
"llm_call",
should_continue,
{
"tool_node": "tool_node", # Continue research loop
"compress_research": "compress_research", # Provide final answer
},
)
researcher_agent_builder.add_edge("tool_node", "llm_call") # Loop back for more research
researcher_agent_builder.add_edge("compress_research", END)
Copied
>_ Output
			
&lt;langgraph.graph.state.StateGraph at 0x1112bddf0&gt;

Compilamos el grafo y representamos el diagrama de flujo

	
< > Input
Python
from langgraph.checkpoint.memory import InMemorySaver
from IPython.display import Image, display
checkpointer = InMemorySaver()
researcher_agent = researcher_agent_builder.compile(checkpointer=checkpointer)
# Visualize the workflow
display(Image(researcher_agent.get_graph(xray=True).draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

Ejecución del agente researcherlink image 32

Primero vamos a llamar al agente Scope para que nos ayude a definir el research_brief

Creamos un hilo para poder hacer varias llamadas al agente, por si nos pide más información

	
< > Input
Python
thread = {"configurable": {"thread_id": "2"}}
user_message = "Quiero investigar las mejores cafeterías de Madrid."
result = await scope_agent.ainvoke({"messages": [HumanMessage(content=f"{user_message}.")]}, config=thread)
Copied
>_ Output
			
⏳ Scope agent:
>_ Output
			
╭───────────────────────────────────────────── 🧑 Real Human Message ─────────────────────────────────────────────╮
│ Quiero investigar las mejores cafeterías de Madrid.. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 2.6s
>_ Output
			
╭─────────────────────────────────── 🤖 Scope Assistant - need clarification? ────────────────────────────────────╮
│ need_clarification=True question='¿Podrías especificar qué criterios te interesan para considerar una cafetería │
│ como "mejor"? Por ejemplo: calidad del café, ambiente, ubicación, opciones de comida, precio, etc. ¿Buscas │
│ cafeterías para trabajar, para socializar, o simplemente para disfrutar buen café?' verification='' │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭──────────────────────────────────────────── 🔧 Scope System Message ────────────────────────────────────────────╮
│ Necesita aclaración por parte del usuario. Enviando pregunta aclaratoria... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

El agente nos pide más información, por lo que volvemos a invocar al agente con la información que nos pide

	
< > Input
Python
user_message = "Examina la calidad del café para evaluar las mejores cafeterías de Madrid."
result = await scope_agent.ainvoke({"messages": [HumanMessage(content=f"{user_message}.")]}, config=thread)
Copied
>_ Output
			
⏳ Scope agent:
>_ Output
			
╭───────────────────────────────────────────── 🧑 Real Human Message ─────────────────────────────────────────────╮
│ Examina la calidad del café para evaluar las mejores cafeterías de Madrid.. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 1.8s
>_ Output
			
╭─────────────────────────────────── 🤖 Scope Assistant - need clarification? ────────────────────────────────────╮
│ need_clarification=False question='' verification='Gracias por la información. Entiendo que deseas un informe │
│ sobre las mejores cafeterías de Madrid, evaluadas principalmente según la calidad del café. Comenzaré la │
│ investigación considerando este criterio como el principal. Ahora iniciaré el proceso de investigación.' │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭──────────────────────────────────────────── 🔧 Scope System Message ────────────────────────────────────────────╮
│ No necesita aclaración por parte del usuario. Enviando mensaje de verificación... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Scope agent - Write research brief:
>_ Output
			
╭─────────────────────────────────── 🤖 Scope Assistant - Write research brief ───────────────────────────────────╮
│ Gracias por la información. Entiendo que deseas un informe sobre las mejores cafeterías de Madrid, evaluadas │
│ principalmente según la calidad del café. Comenzaré la investigación considerando este criterio como el │
│ principal. Ahora iniciaré el proceso de investigación. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 5.5s
>_ Output
			
╭───────────────────────────────── 🤖 Scope Assistant - Research brief generated ─────────────────────────────────╮
│ research_brief='Quiero investigar cuáles son las mejores cafeterías de Madrid evaluadas principalmente según la │
│ calidad del café que ofrecen. Para ello, me interesa identificar y comparar cafeterías en Madrid considerando │
│ factores clave que influyen en la calidad del café, como el origen y la frescura de los granos, los métodos de │
│ tostado, las técnicas de preparación y la experiencia sensorial (sabor, aroma, cuerpo y acidez). No he │
│ especificado preferencias sobre otros aspectos como ambiente, precio, ubicación, opciones de comida o si las │
│ cafeterías son adecuadas para trabajar o socializar, por lo que estos factores pueden considerarse secundarios │
│ o abiertos. Solicito priorizar fuentes primarias y confiables, como sitios web oficiales de las cafeterías, │
│ reseñas de expertos en café y plataformas reconocidas de opinión de usuarios en español.' │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

Ya tenemos el research_brief, por lo que podemos invocar al agente Researcher para que comience la investigación.

IMPORTANTE: En este caso lo que hacemos es invocar al agente Researcher con el research_brief que ya tenemos, para ello se lo pasamos como la variable del estado researcher_messages y como la variable del estado research_topic

Creamos también un cliente de la API de Tavily para poder hacer búsquedas en internet.

	
< > Input
Python
from tavily import TavilyClient
research_brief = result["research_brief"]
research_topic = str(research_brief)
tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
result = await researcher_agent.ainvoke({"researcher_messages": [HumanMessage(content=f"{research_brief}.")], "research_topic": research_topic}, config=thread)
Copied
>_ Output
			
⏳ Researcher agent:
>_ Output
			
╭───────────────────────────────── 🧑 Researcher Agent - Simulated Human Message ─────────────────────────────────╮
│ Quiero investigar cuáles son las mejores cafeterías de Madrid evaluadas principalmente según la calidad del │
│ café que ofrecen. Para ello, me interesa identificar y comparar cafeterías en Madrid considerando factores │
│ clave que influyen en la calidad del café, como el origen y la frescura de los granos, los métodos de tostado, │
│ las técnicas de preparación y la experiencia sensorial (sabor, aroma, cuerpo y acidez). No he especificado │
│ preferencias sobre otros aspectos como ambiente, precio, ubicación, opciones de comida o si las cafeterías son │
│ adecuadas para trabajar o socializar, por lo que estos factores pueden considerarse secundarios o abiertos. │
│ Solicito priorizar fuentes primarias y confiables, como sitios web oficiales de las cafeterías, reseñas de │
│ expertos en café y plataformas reconocidas de opinión de usuarios en español.. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 4.5s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ Voy a investigar las mejores cafeterías de Madrid enfocándome en la calidad del café. Comenzaré con búsquedas │
│ amplias para identificar las cafeterías más reconocidas por la calidad de su café. │
│ │
│ 🔧 Tool Call: tavily_search │
│ Args: { │
│ "query": "mejores cafeterías Madrid calidad café especialidad tostado granos" │
│ } │
│ ID: toolu_01RXjNzCsYFkycrbyZ9idSmz │
│ │
│ 🔧 Tool Call: tavily_search │
│ Args: { │
│ "query": "cafés de especialidad Madrid expertos baristas origen granos" │
│ } │
│ ID: toolu_01DTXgvZwXBdWnwAAvZczcNc │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭───────────────────────────────────── 🔧 Researcher Agent - Should continue ─────────────────────────────────────╮
│ Last message contains tool calls. Continuing to tool execution... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭─────────────────────────────────────── 🔧 Researcher Agent - Tool calls ────────────────────────────────────────╮
│ Tool Call Name: Tool Calls │
│ ID: tool_call_id │
│ │
│ Tool Call 1: │
│ Name: tavily_search │
│ ID: toolu_01RXjNzCsYFkycrbyZ9idSmz │
│ Type: tool_call │
│ Args: │
│ { │
│ "query": "mejores cafeterías Madrid calidad café especialidad tostado granos" │
│ } │
│ │
│ Tool Call 2: │
│ Name: tavily_search │
│ ID: toolu_01DTXgvZwXBdWnwAAvZczcNc │
│ Type: tool_call │
│ Args: │
│ { │
│ "query": "cafés de especialidad Madrid expertos baristas origen granos" │
│ } │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 32.8s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Search results: │
│ │
│ │
│ │
│ --- SOURCE 1: Las 25 mejores cafeterías de especialidad de Madrid --- │
│ URL: https://madridsecreto.co/mejor-cafe-de-madrid/ │
│ │
│ SUMMARY: │
│ &lt;summary&gt; │
│ El artículo presenta una guía detallada de las 27 mejores cafeterías de especialidad en Madrid, destacando la │
│ evolución del café en la ciudad desde el predominio del café torrefacto hasta la actual fiebre por el café de │
│ especialidad o "speciality coffee". Cada cafetería se describe con su nombre, ubicación, características │
│ distintivas y especialidades, como tostado propio, café 100% arábica, opciones veganas, pastelería de alta │
│ calidad, y propuestas innovadoras en bebidas y alimentos. Entre las mencionadas se encuentran Bianchi Kiosko │
│ Caffé en Malasaña, Obrar en Chamberí, Ruda Café en La Latina, The Fix en Argüelles, Toma Café con varios │
│ locales, Pum Pum Café en Lavapiés, Hola Coffee con formación de baristas, y otras como 1000 Cups Café, Aroma │
│ and Bread, Solo Café, Rebel Café, Plántate Café, Cafelito, Monkee Koffee, Feliz Coffee, Acid Café, Santa │
│ Kafeina, Misión Café, Casa Neutrale, Syra Coffee, Caferama, Clima Cafecito, God Café, Café Angélica, HanSo, En │
│ Bruto y Borbeen. El artículo también menciona la importancia de la calidad del grano, la trazabilidad, y la │
...
│ condiciones óptimas.", "En Madrid, tostamos cafés de diferentes orígenes y procesos.", "Recientemente, el día │
│ 17 de junio de 2023 hemos ganado el título de 'mejor tostador de café de especialidad en Madrid'.", "Nuestra │
│ política de precios está hecha para llevar el café de alta calidad a muchos hogares.", "Mejorar la calidad de │
│ vida de todas las personas involucradas en la realización es fundamental para lograr que el café de │
│ especialidad continúe creciendo." │
│ &lt;/key_excerpts&gt; │
│ │
│ -------------------------------------------------------------------------------- │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 29.7s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Search results: │
│ │
│ │
│ │
│ --- SOURCE 1: Café de especialidad en Madrid y España: ¿Cómo comprar? --- │
│ URL: │
│ https://boconospecialtycoffee.com/cafe-de-especialidad-en-madrid/?srsltid=AfmBOoqmKvAdvfWSlCPk0RP95jHwpy9oxBk4T │
│ h5IiSWvBxpNbHDGdYZv │
│ │
│ SUMMARY: │
│ &lt;summary&gt; │
│ Boconó Specialty Coffee es una marca dedicada al tueste y venta de café de especialidad en Madrid, con orígenes │
│ en Venezuela desde 2006 y establecida en Madrid desde 2016. En junio de 2023, ganó el título de mejor tostador │
│ de café de especialidad en Madrid en el evento organizado por Naked Festival. La empresa ofrece cafés de │
│ diferentes orígenes y procesos, además de accesorios, maquinaria y cursos de barista. El café de especialidad │
│ se caracteriza por un proceso meticuloso desde el cultivo sostenible, cosecha manual y postcosecha cuidadosa, │
│ hasta un tostado controlado que preserva sus características únicas. En Madrid, el interés por el café de │
│ especialidad ha crecido notablemente desde 2016, con Lavapiés como uno de los barrios con más cafeterías │
│ especializadas. Boconó tuesta su café en el mismo local y ofrece degustaciones gratuitas, venta en tienda │
...
│ &lt;key_excerpts&gt; │
│ Compra online en nuestra tienda de café de especialidad y recíbelo en cualquier punto de Madrid en 24-48 h., │
│ Café puntuado por encima de 80 en la escala SCA, Tueste artesanal en pequeños lotes, Selección directa de │
│ fincas responsables, Frescura garantizada: tueste bajo pedido, El café de especialidad es un café de alta │
│ calidad, cultivado en condiciones óptimas y puntuado con más de 80 puntos en la escala SCA. │
│ &lt;/key_excerpts&gt; │
│ │
│ -------------------------------------------------------------------------------- │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Researcher agent:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 12.4s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ │
│ 🔧 Tool Call: think_tool │
│ Args: { │
│ "reflection": " He encontrado información muy valiosa en estas primeras búsquedas: **Información │
│ clave obtenida:** 1. Hola Coffee aparece como referencia destacada - puesto 12 a nivel mundial según "The │
│ World's 100 Best Coffee Shops", fundada en 2016, tiene tostadero propio, su dueño fue Spanish Barista Champion │
│ 2016 2. Boconó Specialty Coffee - ganador del título "mejor tostador de café de especialidad en Madrid" en │
│ junio 2023 (Naked Festival), tiene origen venezolano, ubicado en Lavapiés, tuesta en el mismo local 3. Lista │
│ amplia de cafeterías mencionadas: Bianchi Kiosko Caffé, Obrar, Ruda Café, The Fix, Toma Café, Pum Pum Café, │
│ Rebel Café, Café Angélica, entre otras 4. Factores mencionados sobre calidad: granos 100% Arábica, tostado │
│ artesanal, trazabilidad, origen de países como Colombia, Etiopía, Guatemala, Burundi **Gaps │
│ identificados:** - Necesito información más específica sobre métodos de preparación (V60, Chemex, Aeropress, │
│ etc.) - Detalles sobre el perfil sensorial de cada cafetería (sabor, aroma, cuerpo, acidez) - Información de │
│ sitios web oficiales de las cafeterías principales - Opiniones de expertos y reseñas de usuarios en │
│ plataformas reconocidas - Comparación más detallada entre las cafeterías mencionadas **Próximos │
│ pasos:** Necesito buscar información más específica sobre las cafeterías top mencionadas (Hola Coffee, Boconó, │
│ Toma Café, Hanso, Syra Coffee) incluyendo sus sitios oficiales, métodos de preparación, y reseñas de │
│ expertos/usuarios. " │
│ } │
│ ID: toolu_01XneDofDVtm3u5g4CHA4Duf │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭───────────────────────────────────── 🔧 Researcher Agent - Should continue ─────────────────────────────────────╮
│ Last message contains tool calls. Continuing to tool execution... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭─────────────────────────────────────── 🔧 Researcher Agent - Tool calls ────────────────────────────────────────╮
│ Tool Call Name: Tool Calls │
│ ID: tool_call_id │
│ │
│ Tool Call 1: │
│ Name: think_tool │
│ ID: toolu_01XneDofDVtm3u5g4CHA4Duf │
│ Type: tool_call │
│ Args: │
│ { │
│ "reflection": " He encontrado información muy valiosa en estas primeras búsquedas: **Información │
│ clave obtenida:** 1. Hola Coffee aparece como referencia destacada - puesto 12 a nivel mundial según "The │
│ World's 100 Best Coffee Shops", fundada en 2016, tiene tostadero propio, su dueño fue Spanish Barista Champion │
│ 2016 2. Boconó Specialty Coffee - ganador del título "mejor tostador de café de especialidad en Madrid" en │
│ junio 2023 (Naked Festival), tiene origen venezolano, ubicado en Lavapiés, tuesta en el mismo local 3. Lista │
│ amplia de cafeterías mencionadas: Bianchi Kiosko Caffé, Obrar, Ruda Café, The Fix, Toma Café, Pum Pum Café, │
│ Rebel Café, Café Angélica, entre otras 4. Factores mencionados sobre calidad: granos 100% Arábica, tostado │
│ artesanal, trazabilidad, origen de países como Colombia, Etiopía, Guatemala, Burundi **Gaps │
│ identificados:** - Necesito información más específica sobre métodos de preparación (V60, Chemex, Aeropress, │
│ etc.) - Detalles sobre el perfil sensorial de cada cafetería (sabor, aroma, cuerpo, acidez) - Información de │
│ sitios web oficiales de las cafeterías principales - Opiniones de expertos y reseñas de usuarios en │
│ plataformas reconocidas - Comparación más detallada entre las cafeterías mencionadas **Próximos │
│ pasos:** Necesito buscar información más específica sobre las cafeterías top mencionadas (Hola Coffee, Boconó, │
│ Toma Café, Hanso, Syra Coffee) incluyendo sus sitios oficiales, métodos de preparación, y reseñas de │
│ expertos/usuarios. " │
│ } │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 0.0s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Reflection recorded: │
│ He encontrado información muy valiosa en estas primeras búsquedas: │
│ │
│ **Información clave obtenida:** │
│ 1. Hola Coffee aparece como referencia destacada - puesto 12 a nivel mundial según "The World's 100 Best Coffee │
│ Shops", fundada en 2016, tiene tostadero propio, su dueño fue Spanish Barista Champion 2016 │
│ 2. Boconó Specialty Coffee - ganador del título "mejor tostador de café de especialidad en Madrid" en junio │
│ 2023 (Naked Festival), tiene origen venezolano, ubicado en Lavapiés, tuesta en el mismo local │
│ 3. Lista amplia de cafeterías mencionadas: Bianchi Kiosko Caffé, Obrar, Ruda Café, The Fix, Toma Café, Pum Pum │
│ Café, Rebel Café, Café Angélica, entre otras │
│ 4. Factores mencionados sobre calidad: granos 100% Arábica, tostado artesanal, trazabilidad, origen de países │
│ como Colombia, Etiopía, Guatemala, Burundi │
│ │
│ **Gaps identificados:** │
│ - Necesito información más específica sobre métodos de preparación (V60, Chemex, Aeropress, etc.) │
│ - Detalles sobre el perfil sensorial de cada cafetería (sabor, aroma, cuerpo, acidez) │
│ - Información de sitios web oficiales de las cafeterías principales │
│ - Opiniones de expertos y reseñas de usuarios en plataformas reconocidas │
│ - Comparación más detallada entre las cafeterías mencionadas │
│ │
│ **Próximos pasos:** │
│ Necesito buscar información más específica sobre las cafeterías top mencionadas (Hola Coffee, Boconó, Toma │
│ Café, Hanso, Syra Coffee) incluyendo sus sitios oficiales, métodos de preparación, y reseñas de │
│ expertos/usuarios. │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Researcher agent:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 5.1s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ │
│ 🔧 Tool Call: tavily_search │
│ Args: { │
│ "query": "Hola Coffee Madrid sitio oficial métodos preparación barista champion tostadero" │
│ } │
│ ID: toolu_01CQ7xbd5KQM9GoxmXdA4PSd │
│ │
│ 🔧 Tool Call: tavily_search │
│ Args: { │
│ "query": "Toma Café HanSo Syra Coffee Madrid reseñas expertos calidad café métodos extracción" │
│ } │
│ ID: toolu_01Ncd4CFxzPYqQfWpuo3Ev4A │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭───────────────────────────────────── 🔧 Researcher Agent - Should continue ─────────────────────────────────────╮
│ Last message contains tool calls. Continuing to tool execution... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭─────────────────────────────────────── 🔧 Researcher Agent - Tool calls ────────────────────────────────────────╮
│ Tool Call Name: Tool Calls │
│ ID: tool_call_id │
│ │
│ Tool Call 1: │
│ Name: tavily_search │
│ ID: toolu_01CQ7xbd5KQM9GoxmXdA4PSd │
│ Type: tool_call │
│ Args: │
│ { │
│ "query": "Hola Coffee Madrid sitio oficial métodos preparación barista champion tostadero" │
│ } │
│ │
│ Tool Call 2: │
│ Name: tavily_search │
│ ID: toolu_01Ncd4CFxzPYqQfWpuo3Ev4A │
│ Type: tool_call │
│ Args: │
│ { │
│ "query": "Toma Café HanSo Syra Coffee Madrid reseñas expertos calidad café métodos extracción" │
│ } │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 10.2s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Search results: │
│ │
│ │
│ │
│ --- SOURCE 1: En Madrid, Hola Coffee Lagasca se distingue por un diseño ... --- │
│ URL: https://www.instagram.com/reel/DNjXa-9yr7j/ │
│ │
│ SUMMARY: │
│ ✨ Como flagship de la marca tostadora, ofrecen cafés filtrados y bebidas de autor que destacan por su │
│ innovación y calidad, siempre acompañados │
│ │
│ -------------------------------------------------------------------------------- │
│ │
│ │
│ --- SOURCE 2: Hola Coffee Academy --- │
│ URL: │
│ https://hola.coffee/collections/cursos-barista?srsltid=AfmBOopR-kQ7PjyThxry5og5Cvmqc3LPmQfBGCETNBx64KK4NanajSey │
│ │
│ SUMMARY: │
...
│ --- SOURCE 3: Hola Coffee Roasters (@holacoffee) --- │
│ URL: https://www.instagram.com/holacoffee/?hl=en │
│ │
│ SUMMARY: │
│ 72K followers · 1.6K+ following · 725 posts · @holacoffee: “Café de especialidad tostado en Madrid Tostador / │
│ Academia / Cafeterías Calle Doctor Fourquet, │
│ │
│ -------------------------------------------------------------------------------- │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 9.4s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Search results: │
│ │
│ │
│ │
│ --- SOURCE 1: ▷ SYRA Coffee Madrid《 Cafés, Carta y Opiniones 》 --- │
│ URL: https://myspecialtycoffee.es/madrid/syra-coffee/ │
│ │
│ SUMMARY: │
│ &lt;summary&gt; │
│ Syra Coffee es una cadena de cafés de especialidad establecida en Madrid desde 2022, reconocida por ofrecer una │
│ experiencia cafetera de alta calidad con un enfoque en el servicio para llevar. Cuenta con múltiples │
│ ubicaciones en Madrid, todas con horario de lunes a domingo de 8:00 a 20:00, y un diseño minimalista que │
│ prioriza la rapidez y comodidad. Su oferta incluye una variedad de bebidas como Flat White, Latte, Espresso, │
│ café filtrado (V60, Aeropress), además de opciones descafeinadas, tés y repostería artesanal. Destacan la venta │
│ de café en grano de orígenes seleccionados (Etiopía, Colombia, Brasil) y accesorios para preparar café en casa, │
│ así como un programa de fidelización mediante una app móvil que permite acumular puntos y realizar pedidos │
│ anticipados. Syra Coffee también organiza catas de café en colaboración con expertos para profundizar en el │
│ conocimiento del café de especialidad. La marca mantiene una activa presencia en redes sociales (Instagram, │
│ Facebook) y su página web. Los clientes valoran positivamente la calidad del café y la atención, con una │
...
│ https://syra.coffee/pages/opiniones-de-nuestros-clientes?srsltid=AfmBOopS-jJ3HErXYCtp9whNqifE44V062Og1ynxGHmKd0 │
│ OVAPRtdGql │
│ │
│ SUMMARY: │
│ Los clientes saben más. Opiniones de clientes sobre nuestra experiencia de compra en línea. Los clientes │
│ confirman nuestro excelente servicio y calidad. │
│ │
│ -------------------------------------------------------------------------------- │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Researcher agent:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 9.2s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ │
│ 🔧 Tool Call: think_tool │
│ Args: { │
│ "reflection": " **Nueva información obtenida:** 1. Hola Coffee: Tiene academia de baristas con │
│ certificación SCA, tostadora propia en C/ Sepúlveda 109, múltiples cafeterías, más de 10 años de experiencia, │
│ ofrece cursos desde iniciación a avanzado, cafés filtrados y bebidas de autor 2. Syra Coffee: Establecida en │
│ 2022, ofrece métodos de preparación específicos (V60, Aeropress), cafés de origen (Etiopía, Colombia, Brasil), │
│ más de 4.5 estrellas en Google Maps con más de 3000 opiniones, múltiples ubicaciones, catas con expertos 3. │
│ HanSo: Aparece en TripAdvisor pero con información limitada **Información todavía necesaria:** - Detalles │
│ sobre Toma Café (que apareció en la primera búsqueda pero sin profundizar) - Información sobre otras │
│ cafeterías top mencionadas: Bianchi Kiosko Caffé, Obrar, Café Angélica - Reseñas más específicas sobre │
│ perfiles sensoriales - Información oficial de sitios web de las cafeterías **Evaluación:** Tengo │
│ información sólida sobre algunas cafeterías principales, pero necesito una búsqueda más para completar el │
│ panorama con información sobre las otras cafeterías destacadas y obtener más detalles sobre métodos de tostado │
│ y perfiles sensoriales específicos. " │
│ } │
│ ID: toolu_01DKcZGkQjzUYcjB4NQPYRfB │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭───────────────────────────────────── 🔧 Researcher Agent - Should continue ─────────────────────────────────────╮
│ Last message contains tool calls. Continuing to tool execution... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭─────────────────────────────────────── 🔧 Researcher Agent - Tool calls ────────────────────────────────────────╮
│ Tool Call Name: Tool Calls │
│ ID: tool_call_id │
│ │
│ Tool Call 1: │
│ Name: think_tool │
│ ID: toolu_01DKcZGkQjzUYcjB4NQPYRfB │
│ Type: tool_call │
│ Args: │
│ { │
│ "reflection": " **Nueva información obtenida:** 1. Hola Coffee: Tiene academia de baristas con │
│ certificación SCA, tostadora propia en C/ Sepúlveda 109, múltiples cafeterías, más de 10 años de experiencia, │
│ ofrece cursos desde iniciación a avanzado, cafés filtrados y bebidas de autor 2. Syra Coffee: Establecida en │
│ 2022, ofrece métodos de preparación específicos (V60, Aeropress), cafés de origen (Etiopía, Colombia, Brasil), │
│ más de 4.5 estrellas en Google Maps con más de 3000 opiniones, múltiples ubicaciones, catas con expertos 3. │
│ HanSo: Aparece en TripAdvisor pero con información limitada **Información todavía necesaria:** - Detalles │
│ sobre Toma Café (que apareció en la primera búsqueda pero sin profundizar) - Información sobre otras │
│ cafeterías top mencionadas: Bianchi Kiosko Caffé, Obrar, Café Angélica - Reseñas más específicas sobre │
│ perfiles sensoriales - Información oficial de sitios web de las cafeterías **Evaluación:** Tengo │
│ información sólida sobre algunas cafeterías principales, pero necesito una búsqueda más para completar el │
│ panorama con información sobre las otras cafeterías destacadas y obtener más detalles sobre métodos de tostado │
│ y perfiles sensoriales específicos. " │
│ } │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 0.0s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Reflection recorded: │
│ **Nueva información obtenida:** │
│ 1. Hola Coffee: Tiene academia de baristas con certificación SCA, tostadora propia en C/ Sepúlveda 109, │
│ múltiples cafeterías, más de 10 años de experiencia, ofrece cursos desde iniciación a avanzado, cafés filtrados │
│ y bebidas de autor │
│ 2. Syra Coffee: Establecida en 2022, ofrece métodos de preparación específicos (V60, Aeropress), cafés de │
│ origen (Etiopía, Colombia, Brasil), más de 4.5 estrellas en Google Maps con más de 3000 opiniones, múltiples │
│ ubicaciones, catas con expertos │
│ 3. HanSo: Aparece en TripAdvisor pero con información limitada │
│ │
│ **Información todavía necesaria:** │
│ - Detalles sobre Toma Café (que apareció en la primera búsqueda pero sin profundizar) │
│ - Información sobre otras cafeterías top mencionadas: Bianchi Kiosko Caffé, Obrar, Café Angélica │
│ - Reseñas más específicas sobre perfiles sensoriales │
│ - Información oficial de sitios web de las cafeterías │
│ │
│ **Evaluación:** │
│ Tengo información sólida sobre algunas cafeterías principales, pero necesito una búsqueda más para completar el │
│ panorama con información sobre las otras cafeterías destacadas y obtener más detalles sobre métodos de tostado │
│ y perfiles sensoriales específicos. │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Researcher agent:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 4.0s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ │
│ 🔧 Tool Call: tavily_search │
│ Args: { │
│ "query": ""Toma Café" Madrid café especialidad tostado origen granos métodos preparación" │
│ } │
│ ID: toolu_01CnmUWvcWSV8QAvXC8jEnLw │
│ │
│ 🔧 Tool Call: tavily_search │
│ Args: { │
│ "query": "opiniones expertos mejores cafeterías Madrid 2024 2025 calidad baristas perfil sensorial" │
│ } │
│ ID: toolu_01Ec2DAoUYTbkYsLEtdy3US6 │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭───────────────────────────────────── 🔧 Researcher Agent - Should continue ─────────────────────────────────────╮
│ Last message contains tool calls. Continuing to tool execution... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭─────────────────────────────────────── 🔧 Researcher Agent - Tool calls ────────────────────────────────────────╮
│ Tool Call Name: Tool Calls │
│ ID: tool_call_id │
│ │
│ Tool Call 1: │
│ Name: tavily_search │
│ ID: toolu_01CnmUWvcWSV8QAvXC8jEnLw │
│ Type: tool_call │
│ Args: │
│ { │
│ "query": ""Toma Café" Madrid café especialidad tostado origen granos métodos preparación" │
│ } │
│ │
│ Tool Call 2: │
│ Name: tavily_search │
│ ID: toolu_01Ec2DAoUYTbkYsLEtdy3US6 │
│ Type: tool_call │
│ Args: │
│ { │
│ "query": "opiniones expertos mejores cafeterías Madrid 2024 2025 calidad baristas perfil sensorial" │
│ } │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 29.5s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Search results: │
│ │
│ │
│ │
│ --- SOURCE 1: Archivo de la etiqueta: Cafés de orígen --- │
│ URL: https://educafes.com/tag/cafes-de-origen/ │
│ │
│ SUMMARY: │
│ &lt;summary&gt; │
│ La página web Educafes ofrece una amplia gama de contenidos especializados sobre café, destacando temas como la │
│ combinación innovadora de café y queso, la capacitación en manejo y tostión de cafés de origen, y la │
│ importancia del análisis sensorial para valorar la calidad y el sabor del café. Se resalta el emprendimiento │
│ familiar Constantine Café, que promueve la cultura cafetera sostenible en Colombia. Además, se profundiza en la │
│ creciente popularidad del cold brew, su proceso de producción, beneficios, métodos de preparación y su impacto │
│ en el mercado, especialmente entre los millennials. También se aborda la frescura del café como factor clave │
│ para la calidad, explicando los procesos químicos que afectan el sabor y cómo conservarlo. Finalmente, se │
│ menciona a Varietale como un referente en calidad y servicio en Bogotá. │
│ &lt;/summary&gt; │
│ │
...
│ caficultores, reduciendo intermediarios y pagando un precio justo. Hola Coffee ofrece cafés especiales de 80 │
│ puntos en cata, estacionales y de pequeños productores que reciben un precio cuatro veces superior al │
│ comercial. Cafés Tornasol ofrece catas de café bajo reserva para profundizar en el mundo del café de │
│ especialidad sin salir de Madrid. Naji Speciality Coffee destaca por su Naji Pistachio Latte, elaborado con una │
│ mezcla secreta y artesanal de pistacho. │
│ &lt;/key_excerpts&gt; │
│ │
│ -------------------------------------------------------------------------------- │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 11.7s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Search results: │
│ │
│ │
│ │
│ --- SOURCE 1: TOP 10 BEST Cafeteria in Madrid, Spain - Updated 2025 - Yelp --- │
│ URL: https://www.yelp.com/search?find_desc=Cafeteria&amp;find_loc=Madrid │
│ │
│ SUMMARY: │
│ 1. Hanso Café. 4.4 (141 reviews). 0.4 mi. Calle del Pez, 20, 28004 Madrid Spain. 911 375 429 · 2. Lolina │
│ Vintage Café. 4.0 (68 reviews). 0.3 mi. Calle del │
│ │
│ -------------------------------------------------------------------------------- │
│ │
│ │
│ --- SOURCE 2: Las 10 mejores cafeterías de autor en Madrid (actualizado 2025) --- │
│ URL: https://cafecomprar.es/las-10-mejores-cafeterias-de-autor-en-madrid-actualizado-2025-cafecomprar/ │
│ │
│ SUMMARY: │
│ &lt;summary&gt; │
...
│ --- SOURCE 3: Las mejores cafeterías de especialidad de España en 2025 --- │
│ URL: https://www.gastronomistas.com/las-mejores-cafeterias-de-especialidad-de-espana-en-2025/ │
│ │
│ SUMMARY: │
│ Madrid, con 20 locales, y Barcelona, con 17, lideran el listado de mejores cafeterías de especialidad BEST │
│ COFFEE SHOPS de España 2025. │
│ │
│ -------------------------------------------------------------------------------- │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Researcher agent:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 12.2s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ │
│ 🔧 Tool Call: think_tool │
│ Args: { │
│ "reflection": " **Nueva información relevante obtenida:** 1. Toma Café aparece mencionada entre las │
│ mejores cafeterías de autor en Madrid, reconocida por calidad y métodos artesanales 2. Confirmación sobre │
│ HanSo Café con 4.4 estrellas en Yelp (141 reseñas), ubicación en Calle del Pez 20 3. Hola Coffee: ofrece cafés │
│ de 80+ puntos en cata, trabaja con pequeños productores pagando 4 veces el precio comercial 4. Cafés Tornasol │
│ mencionado por ofrecer catas bajo reserva 5. Naji Specialty Coffee destacado por su Naji Pistachio Latte │
│ artesanal 6. Madrid lidera con 20 cafeterías en el ranking Best Coffee Shops España 2025 **Información │
│ sobre calidad del café obtenida:** - Escala SCA: 80-84.99 (muy bueno), 85-89.99 (excelente), 90-100 │
│ (excepcional) - Métodos de preparación mencionados: pour over, espresso, V60, Aeropress, cold brew - Factores │
│ de calidad: compra directa a productores, trazabilidad, tostado artesanal, frescura - Importancia del perfil │
│ sensorial consistente **Evaluación de suficiencia:** Tengo información sólida sobre: - Las principales │
│ cafeterías (Hola Coffee, Syra Coffee, Boconó, Toma Café, HanSo, Naji, Cafés Tornasol) - Factores de calidad │
│ del café (origen, tostado, preparación, puntuación SCA) - Reconocimientos y premios - Métodos de │
│ preparación - Reseñas y opiniones Creo que tengo suficiente información para proporcionar una respuesta │
│ completa. Podría hacer una búsqueda final sobre alguna cafetería específica adicional mencionada (como Bianchi, │
│ Obrar, Café Angélica) pero ya tengo un panorama bastante completo con múltiples opciones bien │
│ fundamentadas. **Decisión:** Realizar una última búsqueda enfocada en detalles específicos sobre perfiles │
│ sensoriales y métodos de tostado de algunas cafeterías top para enriquecer la comparación. " │
│ } │
│ ID: toolu_015YRizf4hcP52akBFMoPk9x │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭───────────────────────────────────── 🔧 Researcher Agent - Should continue ─────────────────────────────────────╮
│ Last message contains tool calls. Continuing to tool execution... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭─────────────────────────────────────── 🔧 Researcher Agent - Tool calls ────────────────────────────────────────╮
│ Tool Call Name: Tool Calls │
│ ID: tool_call_id │
│ │
│ Tool Call 1: │
│ Name: think_tool │
│ ID: toolu_015YRizf4hcP52akBFMoPk9x │
│ Type: tool_call │
│ Args: │
│ { │
│ "reflection": " **Nueva información relevante obtenida:** 1. Toma Café aparece mencionada entre las │
│ mejores cafeterías de autor en Madrid, reconocida por calidad y métodos artesanales 2. Confirmación sobre │
│ HanSo Café con 4.4 estrellas en Yelp (141 reseñas), ubicación en Calle del Pez 20 3. Hola Coffee: ofrece cafés │
│ de 80+ puntos en cata, trabaja con pequeños productores pagando 4 veces el precio comercial 4. Cafés Tornasol │
│ mencionado por ofrecer catas bajo reserva 5. Naji Specialty Coffee destacado por su Naji Pistachio Latte │
│ artesanal 6. Madrid lidera con 20 cafeterías en el ranking Best Coffee Shops España 2025 **Información │
│ sobre calidad del café obtenida:** - Escala SCA: 80-84.99 (muy bueno), 85-89.99 (excelente), 90-100 │
│ (excepcional) - Métodos de preparación mencionados: pour over, espresso, V60, Aeropress, cold brew - Factores │
│ de calidad: compra directa a productores, trazabilidad, tostado artesanal, frescura - Importancia del perfil │
│ sensorial consistente **Evaluación de suficiencia:** Tengo información sólida sobre: - Las principales │
│ cafeterías (Hola Coffee, Syra Coffee, Boconó, Toma Café, HanSo, Naji, Cafés Tornasol) - Factores de calidad │
│ del café (origen, tostado, preparación, puntuación SCA) - Reconocimientos y premios - Métodos de │
│ preparación - Reseñas y opiniones Creo que tengo suficiente información para proporcionar una respuesta │
│ completa. Podría hacer una búsqueda final sobre alguna cafetería específica adicional mencionada (como Bianchi, │
│ Obrar, Café Angélica) pero ya tengo un panorama bastante completo con múltiples opciones bien │
│ fundamentadas. **Decisión:** Realizar una última búsqueda enfocada en detalles específicos sobre perfiles │
│ sensoriales y métodos de tostado de algunas cafeterías top para enriquecer la comparación. " │
│ } │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 0.0s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Reflection recorded: │
│ **Nueva información relevante obtenida:** │
│ 1. Toma Café aparece mencionada entre las mejores cafeterías de autor en Madrid, reconocida por calidad y │
│ métodos artesanales │
│ 2. Confirmación sobre HanSo Café con 4.4 estrellas en Yelp (141 reseñas), ubicación en Calle del Pez 20 │
│ 3. Hola Coffee: ofrece cafés de 80+ puntos en cata, trabaja con pequeños productores pagando 4 veces el precio │
│ comercial │
│ 4. Cafés Tornasol mencionado por ofrecer catas bajo reserva │
│ 5. Naji Specialty Coffee destacado por su Naji Pistachio Latte artesanal │
│ 6. Madrid lidera con 20 cafeterías en el ranking Best Coffee Shops España 2025 │
│ │
│ **Información sobre calidad del café obtenida:** │
│ - Escala SCA: 80-84.99 (muy bueno), 85-89.99 (excelente), 90-100 (excepcional) │
│ - Métodos de preparación mencionados: pour over, espresso, V60, Aeropress, cold brew │
│ - Factores de calidad: compra directa a productores, trazabilidad, tostado artesanal, frescura │
│ - Importancia del perfil sensorial consistente │
│ │
│ **Evaluación de suficiencia:** │
│ Tengo información sólida sobre: │
│ - Las principales cafeterías (Hola Coffee, Syra Coffee, Boconó, Toma Café, HanSo, Naji, Cafés Tornasol) │
│ - Factores de calidad del café (origen, tostado, preparación, puntuación SCA) │
│ - Reconocimientos y premios │
│ - Métodos de preparación │
│ - Reseñas y opiniones │
│ │
│ Creo que tengo suficiente información para proporcionar una respuesta completa. Podría hacer una búsqueda final │
│ sobre alguna cafetería específica adicional mencionada (como Bianchi, Obrar, Café Angélica) pero ya tengo un │
│ panorama bastante completo con múltiples opciones bien fundamentadas. │
│ │
│ **Decisión:** Realizar una última búsqueda enfocada en detalles específicos sobre perfiles sensoriales y │
│ métodos de tostado de algunas cafeterías top para enriquecer la comparación. │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Researcher agent:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 3.2s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ │
│ 🔧 Tool Call: tavily_search │
│ Args: { │
│ "query": ""Café Angélica" "Obrar" "Bianchi" Madrid café especialidad tostado perfil sensorial │
│ Colombia Etiopía" │
│ } │
│ ID: toolu_017pXguKFjDdb3cWhbwQ7ugp │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭───────────────────────────────────── 🔧 Researcher Agent - Should continue ─────────────────────────────────────╮
│ Last message contains tool calls. Continuing to tool execution... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭─────────────────────────────────────── 🔧 Researcher Agent - Tool calls ────────────────────────────────────────╮
│ Tool Call Name: Tool Calls │
│ ID: tool_call_id │
│ │
│ Tool Call 1: │
│ Name: tavily_search │
│ ID: toolu_017pXguKFjDdb3cWhbwQ7ugp │
│ Type: tool_call │
│ Args: │
│ { │
│ "query": ""Café Angélica" "Obrar" "Bianchi" Madrid café especialidad tostado perfil sensorial │
│ Colombia Etiopía" │
│ } │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 31.9s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Search results: │
│ │
│ │
│ │
│ --- SOURCE 1: Vogue Enhorabuena cafeteros, tenéis un nuevo templo en Madrid: Café Angélica --- │
│ URL: https://www.vogue.es/living/articulos/cafe-angelica-madrid-hermanos-zamora-san-bernardo/29259 │
│ │
│ SUMMARY: │
│ &lt;summary&gt; │
│ Café Angélica es un nuevo café en Madrid ubicado en la calle San Bernardo, 24, que ha recuperado un local │
│ histórico que fue la primera tienda de hierbas aromáticas de la ciudad, fundada en 1948 por Francisco. Carlos y │
│ Lucía Zamora, responsables del grupo de restauración Deluz, adquirieron el espacio tras el cierre por aumento │
│ de alquiler y lo transformaron en un café para amantes del café de especialidad, que ellos mismos tuestan con │
│ una tostadora Probat alemana y sirven con una máquina La Marzocco. Ofrecen desayunos con productos ecológicos │
│ como queso fresco pasiego, bocadillos con roast beef de Cantabria, ensaladas con pollo ecológico de Segovia y │
│ vinos seleccionados. Además, venden hierbas naturales, especias, tés especiales, frutos secos y cafés de │
│ pequeños agricultores ecológicos de Colombia, México, Etiopía y Kenia. La iniciativa nació tras ver el │
│ documental "Black Gold" y busca apoyar a pequeños productores y un proyecto de cooperación en Costa de Marfil │
│ liderado por Philo, colaboradora cercana de los Zamora. Café Angélica combina tradición, sostenibilidad y │
...
│ sostenible en el que cuidan a todas las personas implicadas en el desarrollo, desde su recolección grano a │
│ grano, hasta la fase de tostado., Estos cafés proceden de pequeñas cooperativas de Guatemala, Etiopía, Ruanda o │
│ Colombia, entre otros. Todos, sin excepción, son cafés con una historia social detrás y eso, además de sus nota │
│ de cata, los hace si cabe todavía más exquisitos., Organizan cursos y catas todos los jueves a partir de las │
│ 20pm, allí mismo, en Café Angélica. │
│ &lt;/key_excerpts&gt; │
│ │
│ -------------------------------------------------------------------------------- │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Researcher agent:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 16.6s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ │
│ 🔧 Tool Call: think_tool │
│ Args: { │
│ "reflection": " **Excelente información adicional obtenida sobre Café Angélica:** - Ubicación: C/ San │
│ Bernardo 24, local histórico desde 1948 - Tuestan su propio café con tostadora Probat alemana - Máquina La │
│ Marzocco para preparación - Cafés de comercio justo de Colombia, México, Etiopía, Kenia, Guatemala, Ruanda - │
│ Organizan catas todos los jueves a las 20:00h - Proyecto sostenible y con enfoque social (apoyo a pequeños │
│ productores) - Recomendado por Vogue y otras publicaciones especializadas - Grupo Deluz (Carlos y Lucía │
│ Zamora) **Resumen completo de información recopilada:** He conseguido información detallada sobre │
│ múltiples cafeterías de especialidad en Madrid con enfoque en calidad del café: 1. **Hola Coffee** - Puesto │
│ 12 mundial, tostadora propia, academia SCA, Spanish Barista Champion 2016 2. **Boconó Specialty Coffee** - │
│ Mejor tostador Madrid 2023, tuesta en local, Lavapiés 3. **Syra Coffee** - Métodos V60 y Aeropress, +4.5 │
│ estrellas Google Maps (3000+ reseñas), cafés de Etiopía/Colombia/Brasil 4. **Café Angélica** - Tostadora │
│ Probat, máquina La Marzocco, cafés de comercio justo, catas semanales 5. **Toma Café** - Reconocida entre │
│ cafeterías de autor, métodos artesanales 6. **HanSo Café** - 4.4 estrellas Yelp, 141 reseñas 7. **Naji │
│ Specialty Coffee** - Especialidad en Pistachio Latte artesanal 8. **Cafés Tornasol** - Catas bajo │
│ reserva También tengo información sobre: - Factores de calidad: escala SCA (80+ puntos), origen de granos, │
│ tostado artesanal, métodos de preparación - Métodos: pour over, V60, Aeropress, espresso, cold brew - │
│ Orígenes principales: Colombia, Etiopía, Guatemala, Brasil, Kenia, Ruanda, México **Evaluación │
│ final:** Tengo información muy completa y de fuentes confiables (Vogue, rankings internacionales, sitios │
│ especializados, sitios oficiales, reseñas de usuarios). Puedo proporcionar una respuesta exhaustiva comparando │
│ las mejores cafeterías según calidad del café. He realizado 5 búsquedas que es el límite para consultas │
│ complejas. **Decisión:** Proceder a elaborar la respuesta final con toda la información recopilada. " │
│ } │
│ ID: toolu_01W3EfSjdzUbPcy5TrCGSVNX │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭───────────────────────────────────── 🔧 Researcher Agent - Should continue ─────────────────────────────────────╮
│ Last message contains tool calls. Continuing to tool execution... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭─────────────────────────────────────── 🔧 Researcher Agent - Tool calls ────────────────────────────────────────╮
│ Tool Call Name: Tool Calls │
│ ID: tool_call_id │
│ │
│ Tool Call 1: │
│ Name: think_tool │
│ ID: toolu_01W3EfSjdzUbPcy5TrCGSVNX │
│ Type: tool_call │
│ Args: │
│ { │
│ "reflection": " **Excelente información adicional obtenida sobre Café Angélica:** - Ubicación: C/ San │
│ Bernardo 24, local histórico desde 1948 - Tuestan su propio café con tostadora Probat alemana - Máquina La │
│ Marzocco para preparación - Cafés de comercio justo de Colombia, México, Etiopía, Kenia, Guatemala, Ruanda - │
│ Organizan catas todos los jueves a las 20:00h - Proyecto sostenible y con enfoque social (apoyo a pequeños │
│ productores) - Recomendado por Vogue y otras publicaciones especializadas - Grupo Deluz (Carlos y Lucía │
│ Zamora) **Resumen completo de información recopilada:** He conseguido información detallada sobre │
│ múltiples cafeterías de especialidad en Madrid con enfoque en calidad del café: 1. **Hola Coffee** - Puesto │
│ 12 mundial, tostadora propia, academia SCA, Spanish Barista Champion 2016 2. **Boconó Specialty Coffee** - │
│ Mejor tostador Madrid 2023, tuesta en local, Lavapiés 3. **Syra Coffee** - Métodos V60 y Aeropress, +4.5 │
│ estrellas Google Maps (3000+ reseñas), cafés de Etiopía/Colombia/Brasil 4. **Café Angélica** - Tostadora │
│ Probat, máquina La Marzocco, cafés de comercio justo, catas semanales 5. **Toma Café** - Reconocida entre │
│ cafeterías de autor, métodos artesanales 6. **HanSo Café** - 4.4 estrellas Yelp, 141 reseñas 7. **Naji │
│ Specialty Coffee** - Especialidad en Pistachio Latte artesanal 8. **Cafés Tornasol** - Catas bajo │
│ reserva También tengo información sobre: - Factores de calidad: escala SCA (80+ puntos), origen de granos, │
│ tostado artesanal, métodos de preparación - Métodos: pour over, V60, Aeropress, espresso, cold brew - │
│ Orígenes principales: Colombia, Etiopía, Guatemala, Brasil, Kenia, Ruanda, México **Evaluación │
│ final:** Tengo información muy completa y de fuentes confiables (Vogue, rankings internacionales, sitios │
│ especializados, sitios oficiales, reseñas de usuarios). Puedo proporcionar una respuesta exhaustiva comparando │
│ las mejores cafeterías según calidad del café. He realizado 5 búsquedas que es el límite para consultas │
│ complejas. **Decisión:** Proceder a elaborar la respuesta final con toda la información recopilada. " │
│ } │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 0.0s
>_ Output
			
╭──────────────────────────────────── 🔧 Researcher Agent - Tool calls result ────────────────────────────────────╮
│ Reflection recorded: │
│ **Excelente información adicional obtenida sobre Café Angélica:** │
│ - Ubicación: C/ San Bernardo 24, local histórico desde 1948 │
│ - Tuestan su propio café con tostadora Probat alemana │
│ - Máquina La Marzocco para preparación │
│ - Cafés de comercio justo de Colombia, México, Etiopía, Kenia, Guatemala, Ruanda │
│ - Organizan catas todos los jueves a las 20:00h │
│ - Proyecto sostenible y con enfoque social (apoyo a pequeños productores) │
│ - Recomendado por Vogue y otras publicaciones especializadas │
│ - Grupo Deluz (Carlos y Lucía Zamora) │
│ │
│ **Resumen completo de información recopilada:** │
│ │
│ He conseguido información detallada sobre múltiples cafeterías de especialidad en Madrid con enfoque en calidad │
│ del café: │
│ │
│ 1. **Hola Coffee** - Puesto 12 mundial, tostadora propia, academia SCA, Spanish Barista Champion 2016 │
│ 2. **Boconó Specialty Coffee** - Mejor tostador Madrid 2023, tuesta en local, Lavapiés │
│ 3. **Syra Coffee** - Métodos V60 y Aeropress, +4.5 estrellas Google Maps (3000+ reseñas), cafés de │
│ Etiopía/Colombia/Brasil │
│ 4. **Café Angélica** - Tostadora Probat, máquina La Marzocco, cafés de comercio justo, catas semanales │
│ 5. **Toma Café** - Reconocida entre cafeterías de autor, métodos artesanales │
│ 6. **HanSo Café** - 4.4 estrellas Yelp, 141 reseñas │
│ 7. **Naji Specialty Coffee** - Especialidad en Pistachio Latte artesanal │
│ 8. **Cafés Tornasol** - Catas bajo reserva │
│ │
│ También tengo información sobre: │
│ - Factores de calidad: escala SCA (80+ puntos), origen de granos, tostado artesanal, métodos de preparación │
│ - Métodos: pour over, V60, Aeropress, espresso, cold brew │
│ - Orígenes principales: Colombia, Etiopía, Guatemala, Brasil, Kenia, Ruanda, México │
│ │
│ **Evaluación final:** │
│ Tengo información muy completa y de fuentes confiables (Vogue, rankings internacionales, sitios especializados, │
│ sitios oficiales, reseñas de usuarios). Puedo proporcionar una respuesta exhaustiva comparando las mejores │
│ cafeterías según calidad del café. He realizado 5 búsquedas que es el límite para consultas complejas. │
│ │
│ **Decisión:** Proceder a elaborar la respuesta final con toda la información recopilada. │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Researcher agent:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 53.0s
>_ Output
			
╭───────────────────────────────────────── 🤖 Researcher Agent response ──────────────────────────────────────────╮
│ Basándome en mi investigación exhaustiva, te presento un análisis detallado de las **mejores cafeterías de │
│ Madrid evaluadas según la calidad del café**, priorizando los factores clave que mencionaste: │
│ │
│ --- │
│ │
│ ## 🏆 **LAS MEJORES CAFETERÍAS DE ESPECIALIDAD EN MADRID** │
│ │
│ ### **1. HOLA COFFEE** ⭐ Top 1 en Madrid │
│ **Reconocimientos:** │
│ - **Puesto 12 mundial** en el ranking "The World's 100 Best Coffee Shops" 2025 │
│ - Fundador nombrado **Spanish Barista Champion 2016** │
│ - Madrid lidera en España con 20 cafeterías en el ranking nacional │
│ │
│ **Calidad del Café:** │
│ - **Origen y frescura:** Cafés de pequeños productores con puntuación **superior a 80 puntos** en la escala SCA │
│ (Specialty Coffee Association) │
│ - **Tostado:** Tostadora propia ubicada en Calle Sepúlveda 109, Madrid │
│ - **Métodos de preparación:** Cafés filtrados, espresso y bebidas de autor │
│ - **Trazabilidad:** Pagan hasta **4 veces el precio comercial** a productores para garantizar calidad ética │
...
│ - **Plataformas de reseñas:** Google Maps (3,000+ opiniones Syra Coffee), Yelp (141 reseñas HanSo) │
│ - **Organizaciones certificadoras:** Specialty Coffee Association (SCA), Naked Festival │
│ │
│ --- │
│ │
│ Todas estas cafeterías cumplen con los más altos estándares de calidad en café de especialidad, con │
│ puntuaciones superiores a 80 en la escala SCA, trazabilidad completa, métodos de preparación profesionales y un │
│ compromiso genuino con la excelencia sensorial. La elección entre ellas dependerá de tu ubicación preferida en │
│ Madrid y si valoras más el reconocimiento internacional, el proyecto sostenible o la experiencia educativa. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
╭──────────────────────────────── 🔧 Researcher Agent - Should compress research ─────────────────────────────────╮
│ No tool calls found. Stopping research and compressing findings... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
>_ Output
			
⏳ Compressing Research:
|▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| in 54.6s
>_ Output
			
╭─────────────────────────────────── 🤖 Researcher Agent - Compressed Research ───────────────────────────────────╮
│ **Lista de consultas y llamadas a herramientas realizadas** │
│ │
│ 1. tavily_search: "mejores cafeterías Madrid calidad café especialidad tostado granos" │
│ 2. tavily_search: "cafés de especialidad Madrid expertos baristas origen granos" │
│ 3. tavily_search: "Hola Coffee Madrid sitio oficial métodos preparación barista champion tostadero" │
│ 4. tavily_search: "Toma Café HanSo Syra Coffee Madrid reseñas expertos calidad café métodos extracción" │
│ 5. tavily_search: "Toma Café Madrid café especialidad tostado origen granos métodos preparación" │
│ 6. tavily_search: "opiniones expertos mejores cafeterías Madrid 2024 2025 calidad baristas perfil sensorial" │
│ 7. tavily_search: ""Café Angélica" "Obrar" "Bianchi" Madrid café especialidad tostado perfil sensorial │
│ Colombia Etiopía" │
│ │
│ --- │
│ │
│ **Resultados completos** │
│ │
│ --- SOURCE 1: Las 25 mejores cafeterías de especialidad de Madrid --- │
│ URL: https://madridsecreto.co/mejor-cafe-de-madrid/ │
│ │
│ SUMMARY: │
...
│ [9] TOP 10 BEST Cafeteria in Madrid, Spain - Updated 2025 - Yelp: │
│ https://www.yelp.com/search?find_desc=Cafeteria&amp;find_loc=Madrid │
│ [10] Las 10 mejores cafeterías de autor en Madrid (actualizado 2025): │
│ https://cafecomprar.es/las-10-mejores-cafeterias-de-autor-en-madrid-actualizado-2025-cafecomprar/ │
│ [11] Vogue Enhorabuena cafeteros, tenéis un nuevo templo en Madrid: Café Angélica: │
│ https://www.vogue.es/living/articulos/cafe-angelica-madrid-hermanos-zamora-san-bernardo/29259 │
│ [12] Cafeangelica Café Angélica – Tienda de Café, tes y especias en Madrid: https://cafeangelica.es/ │
│ [13] Con El Morro Fino Café Angélica, templo del café, té y especias | Con El Morro Fino: │
│ https://conelmorrofino.com/cafe-angelica-madrid/ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

Cómo podemos ver el agente Researcher ha hecho uso varias veces de la tool tavily_search para obtener información de internet y de la tool think_tool para pensar sobre la información que tiene. Por último ha comprimido la información

---

➡️ **Continúa en la Parte 2: Research Supervisor multiagente**, donde coordinaremos varios agentes Researcher en paralelo.

Seguir leyendo

Últimos posts -->

¿Has visto estos proyectos?

Gymnasia

Gymnasia Gymnasia
React Native
Expo
TypeScript
FastAPI
Next.js
OpenAI
Anthropic

Aplicación móvil de entrenamiento personal con asistente de IA, biblioteca de ejercicios, seguimiento de rutinas, dieta y medidas corporales

Horeca chatbot

Horeca chatbot Horeca chatbot
Python
LangChain
PostgreSQL
PGVector
React
Kubernetes
Docker
GitHub Actions

Chatbot conversacional para cocineros de hoteles y restaurantes. Un cocinero, jefe de cocina o camaeror de un hotel o restaurante puede hablar con el chatbot para obtener información de recetas y menús. Pero además implementa agentes, con los cuales puede editar o crear nuevas recetas o menús

Naviground

Naviground Naviground
Ver todos los proyectos -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres ver alguna charla?

Últimas charlas -->

¿Quieres mejorar con estos tips?

Últimos tips -->

Usa esto en local

Los espacios de Hugging Face nos permite ejecutar modelos con demos muy sencillas, pero ¿qué pasa si la demo se rompe? O si el usuario la elimina? Por ello he creado contenedores docker con algunos espacios interesantes, para poder usarlos de manera local, pase lo que pase. De hecho, es posible que si pinchas en alún botón de ver proyecto te lleve a un espacio que no funciona.

Flow edit

Flow edit Flow edit

Edita imágenes con este modelo de Flow. Basándose en SD3 o FLUX puedes editar cualquier imagen y generar nuevas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos los contenedores -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres entrenar tu modelo con estos datasets?

short-jokes-dataset

HuggingFace

Dataset de chistes en inglés

Uso: Fine-tuning de modelos de generación de texto humorístico

231K filas 2 columnas 45 MB
Ver en HuggingFace →

opus100

HuggingFace

Dataset con traducciones de inglés a español

Uso: Entrenamiento de modelos de traducción inglés-español

1M filas 2 columnas 210 MB
Ver en HuggingFace →

netflix_titles

HuggingFace

Dataset con películas y series de Netflix

Uso: Análisis de catálogo de Netflix y sistemas de recomendación

8.8K filas 12 columnas 3.5 MB
Ver en HuggingFace →
Ver más datasets -->