Build a Deep Research AI Agent with LangGraph (1/3)

Build a Deep Research AI Agent with LangGraph (1/3)

Disclaimer: This post has been translated to English using a machine translation model. Please, let me know if you find any mistakes.

📚 **This post is part of the _Deep Research with LangGraph_ series**, divided into three chapters that are read in order:

> * 👉 **Part 1: Scope and Researcher Agents**> * Part 2: Multi-agent Research Supervisor

* Part 3: Writer Agent and final report

Have you ever gone looking for the best something? The best running shoes, the best tool for that repair, the best barbecue?

When you search among the 80 pages that appear, 90% are pages to make money with Amazon affiliates, made by people who don’t know what they’re writing about. They just look for the products that sell the most on Amazon, stuff the page with keywords to appear in the top search results on Google, and fill the content with links to Amazon.

In the remaining 10%, you have to navigate pages with huge blocks of text, designed to improve SEO and appear in the top search results on Google.

So a simple Google search turns into a tedious and boring task.

Another thing is when you want to research a topic that doesn’t require purchases. As before, you have to browse dozens of pages, cross-checking information, following sources, etc.

What if we ask an AI assistant to take care of that?

In this post, we’re going to explain how to create your own AI assistant so it can research any topic for you, allowing you to dedicate your time to what really matters.

**Information**: This post is based on the course Deep Research with LangGraph from LangChain Academy. But since I felt there were many concepts taken for granted, I wrote it in the way I would have liked to find it

What is an AI assistant?link image 34

An AI assistant is an agent that can perform complex and automated tasks. But in the current era of agent development, we have to design it very carefully so that it doesn’t go down the rabbit hole like Alice in Wonderland and knows when to stop searching and when it has already found the information you needed.

Assistant Architecturelink image 35

To build the assistant, we will use the following architecture:

deep researcher architecture

Scopelink image 36

First, we are going to define the scope of the investigation. An LLM will analyze the user's message and, if it considers that it needs more information, it will ask for it.

Researcher supervisorlink image 37

Here an LLM will be responsible for supervising the research, dividing it into small investigations and assigning them to researcher subagents. Each of those subagents will have tools to be able to carry out the research.

Each investigative sub-agent will use its tools several times to obtain an in-depth investigation of its task, and when finished it will return a summary of its investigation to the supervisor. Each sub-agent has its own context and does not have access to the context and results of the other sub-agents.

As soon as the supervisor receives all the summaries from the subagents, it will evaluate whether it needs to request further investigations from the subagents. Once it considers that no further investigations are necessary, it will prepare a summary of all the information it has and send it to the writer.

Writerlink image 38

Finally, once the in-depth research has been completed, the writer is responsible for writing a complete and detailed report for the user.

Library Installationlink image 39

Let's install all the libraries we will need for the assistant development.

First, we create a uv environment

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

We synchronized it

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

We turned it on

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

We installed the necessary libraries

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

Let's analyze what we've installed

Utilslink image 40

Before diving into the code, we're going to create some functions that will be useful for the development of the assistant.

Directorylink image 41

First, we create a function that allows us to obtain the current directory.

	
< > 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 42

Now we create a function that allows us to get the current date.

	
< > 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 43

Finally, we created a series of functions that allow us to format messages so they are displayed more clearly in the console

	
< > 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 44

Let's start designing the first module, the scope module. With this module, we will define the scope of the research.

Scope promptslink image 45

Let's start by defining the scope prompts. First, we define the prompt that makes the LLM ask the user for more information when it considers that it does not have enough information.

  • First, the context of the conversation held up to that moment is passed to it
  • The date is passed in, which is why we had to create a function to obtain the date* You are instructed to ask the user clarifying questions, if necessary. If the user asks to investigate something and the LLM considers that it needs more information than what the user wants, it can ask for more information. If it does not need more information, it must tell the user that it has everything it needs to start the investigation
  • The output format to be returned is indicated to you
	
< > 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

Now we define the prompt so that, with the conversation the LLM has had with the user, the LLM generates a prompt with the research that needs to be carried out and that will go to the research agent.

  • The conversation context that has been carried on up to that point is passed to it
  • The date is indicated to them
  • It specifies what the format of the prompt that it has to generate should be like. Things the user has not said cannot be assumed. It has to be specific and detailed. Write it in the first person
	
< > 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

Estado del alcancelink image 46

We now define the scope state. The state is like the agent's short-term memory, where the variables and data that will be used during the agent's execution and that need to be available throughout the agent's execution are stored.

As soon as the agent stops running, that memory will be lost

Let's define two states

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

It may seem odd to create two states, AgentInputState and AgentState. We could have created a single state called AgentState, since the two we created inherit from the MessagesState class. Therefore, whether we define just one or define two, the messages that are generated, both those coming from the user and those generated by the agent, will be stored in a list called messages.

But we have done it in two, because we are going to define AgentInputState as the input format that the agent will expect, and AgentState as the agent’s internal state. Why? So that the rest of the state variables (research_brief, supervisor_messages, raw_notes, notes, final_report) cannot be modified when making a call to the agent.

For example, if we had created only an AgentState, when invoking the agent, we could do

state = {
"messages": ["Hello, how are you?"],
"research_brief": "Research the factors that influence coffee quality",

json

"supervisor_messages": [],

"raw_notes": [],
"notes": [],
"final_report": ""
}

agent.invoke(state)

When doing this, there would initially be a value for research_brief, and we don’t want that; we want the agent to be the one to generate the research_brief.

By having an AgentState and an AgentInputState, we will set the AgentState as a variable when invoking the agent to AgentInputState, so that we will not be able to modify the research_brief from outside the agent.

Structured output of the Scope agentlink image 47

We now define what we want the LLM output to look like when it is invoked. We define two structured outputs, one for when we want to know whether we need more data from the user (ClarifyWithUser) and another for when we want to generate the 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

The Field function in pydantic is used to define the type of data expected as the LLM output and to add a description of what is expected the output to be.

With ClarifyWithUser, the LLM is expected to return

  • a boolean (need_clarification) indicating whether more information is needed from the user
  • a question (question) that, if more information from the user is needed, will be asked to clarify the scope of the investigation
  • a verification message (verification) that will be given to the user so they can confirm they have enough information to begin the investigation

With ResearchQuestion, the LLM is expected to return a research question (research_brief), which is the one that will be sent to the research agent so that the research can be conducted.

Scope agent language modellink image 48

Now that we have the prompts, we define the language model that the agent will use.

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

Scope Agent Functionslink image 49

Since the agent will have to perform two actions, ask the user for more information, if necessary, and generate the research_brief, we will create two functions for each of the actions.

We start with the function to ask the user for more information

  • First, we obtain the last message from the message history, which will be the user's message
  • Then we make a call to the model forcing it to return a structured output of the ClarifyWithUser type* If the LLM returns that more information is needed, we send a message to the user indicating that more information is needed and the agent flow is terminated
  • If the LLM returns that no more information is needed, we send a message to the user indicating that the investigation can begin, and in the agent flow it is indicated that the write_research_brief function should be executed to generate the 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

We now write the function to generate the research_brief

  • First, we get the latest message from the message history, which will be
  • Then we make a call to the model, forcing it to return a structured output of the ResearchQuestion type
  • The agent flow ends by returning the 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

Scope Agent Graphlink image 50

Now that we have everything we need to build the agent, let's define the agent graph.

First, we create the agent with the AgentState and the AgentInputState as we explained earlier, so that only the message of the agent's internal state can be modified and the rest of the state variables cannot be modified.

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

We add two nodes, each with the functions that we have defined before

	
< > 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;

And finally we add the edges that connect the nodes. We start with the START node and connect it to the clarify_with_user node. On the other hand, we connect the write_research_brief node to the END node. And it is the clarify_with_user function that is responsible for deciding whether, with the user's message, we move on to the write_research_brief node or ask the user for more information and move to the END node.

	
< > 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;

We compile the graph and represent the flow diagram

	
< > 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;

Scope Agent Executionlink image 51

We create a thread so we can make several calls to the agent, in case it asks us for more information, and we create the initial user message with what we want it to create the 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... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

As you can see, the agent asks us for more information, so we invoke the agent again with the information it requests

	
< > 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.' │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

With the new clarifying information, the agent has generated the research_brief, which will be what we send to the research agent.

Researcherlink image 52

If we remember how the deep researcher architecture was going to be, we see that there is a Research supervisor who is in charge of assigning research tasks to different Researchers

deep researcher architecture

So now we are going to define the Researcher that will be responsible for carrying out the research.

Researcher promptslink image 53

First we define the prompt for the Researcher, indicating to it

  • What is a tool-enabled researcher
  • They are told that the two tools they have are web search and reflection
  • You are given instructions on how the research should be carried out
  • You are instructed to use the reflection tool after each search
  • A tool-call budget is given to it
	
< > 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

Now we define the prompt of an LLM that summarizes content from a web page.

  • You are given the content of a web page
  • You are given instructions on how the summary should be carried out* It is given a format in which it must return the summary
  • The current date is given to it
	
< > 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

Once the content of the web page has been summarized, another LLM is called to clean up the research results and the internal reasoning used when summarizing the content.

  • Research content is provided to them
  • You are given some instructions on how to perform the summary
  • A format is provided in which it must return the summary* The current date is given to it
  • You are instructed to preserve all statements and relevant information collected by the investigator
  • They are instructed to repeat the key information verbatim

This prompt is important, because it asks you to create a numbered list with the sources used. This is useful so that in future research the same sources are not used again. But, above all, so that in the final report to the user, the sources used for each assertion can be shown.

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

Finally, we define the prompt for an LLM responsible for refining the research results for the specific question assigned to it.

  • You are given the research topic* You are given instructions on how the summary should be carried out
	
< > 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 54

Now we have something similar to the Scope state: we have two states, ResearcherState and ResearcherInputState. Let's look at them and then explain it.

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

It can be seen that all the variables of ResearcherOutputState (compressed_research, raw_notes, and researcher_messages) are present in ResearcherState, but the variables tool_call_iterations and research_topic of ResearcherState are not in ResearcherOutputState.

This is because they are variables that the research agent needs, but they are not relevant to the end user, who in this case will be the Supervisor.

Structured output from the Researcherlink image 55

Let's define what we want the Researcher output to be like.

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

We see that we want a summary and the key quotes from the research.

Researcher Toolslink image 56

When we explain the Scope agent, we explain the agent’s functions, whereas now we are going to define the tools; what is the difference?

We have implemented the functions ourselves, and they will always run whenever the code follows a flow that we have defined. However, the tools are instruments that the agent has, and it is the agent who decides whether to use them or not, and when to use them.

To search the internet, we are going to use the tavily_search tool that we have already imported before.

Before defining the tool, we first define some auxiliary functions that will help us define the tool.

First, we define a function to perform several searches in parallel.

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

Since we perform several searches in parallel, those searches may have found data from the same sources, so we discard duplicate searches with a function

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

Now we define a function to summarize the content of a web page; for this, we are going to use the summarize_webpage_prompt 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

With the search content summarized, we create a function to clean up the research results.

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

And finally, we created a function to give the research content a specific format.

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

With all these auxiliary functions, we can now define the tool that will use the functions we have defined. First, it performs several searches in parallel, discards duplicate results, cleans the results, and gives the research content a specific format.

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

You can see that we have used parse_docstring=True when defining the tool. This causes the function docstring to be passed to the LLM so it has that information as context. If we did not do that, the LLM would only receive as context

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

That is, it would only know what type of arguments the function expects, but it would not know what it is used for. But by setting parse_docstring=True, what the LLM receives is something like this:

I have a search engine that:
- Receives:
- query (string): "A simple search query"
- max_results (int): "Maximum number of results to return"
- topic (Literal): "Category to filter by ('general', 'news', 'finance')"

Although there is another thing we have done that means it is not really like that, the lines

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

They use InjectedToolArg, which makes the values of those arguments fixed and not decided by the LLM. Imagine the LLM decides that the maximum number of results should be 100; that could make the search take too long.

So, due to this, what the LLM actually receives is something like this:

I have a search engine that:
- Receives:
- query (string): "A simple search query"

Reflection Toollink image 58

As we want the agent to reflect after each search, we provide it with a tool for that purpose

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

As before, we use parse_docstring=True so that the docstring is passed to the LLM as context.

Researcher language modelslink image 59

Now that we have defined the tools, we are going to define the different language models that we will use, since some of them will use the tools

Researcher summary language modellink image 60

First, we are going to create the language model that we will use in the summarize_webpage_content function that we defined earlier

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

Research language modellink image 61

Now we are going to define the language model that will be in charge of the research

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

Now we give the research language model access to the tools we have defined

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

Information compression language modellink image 62

Finally, we define a language model that will be responsible for compressing the research information

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

Agent Functionslink image 63

As we have seen before, on one hand there are the agent's tools, which are tools that the LLM will choose when and how to use, and on the other hand there are the agent's functions, which are the functions that will be executed in the graph nodes, and they will be executed at the moment we define by code.

First, we create a function that will be responsible for calling the research language model with the tools it has access to. The model will decide whether it needs to use a tool or not, and if it does, control will be passed to the tool so it can be executed.

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

If the research language model decides that it does not need to use a tool, control is passed to the tool_node function so that the tool chosen by the LLM can be executed.

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

Now we are going to define the function that will be responsible for compressing the research information.

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

Lastly, we are going to define the function that will be responsible for deciding whether the investigation continues or is concluded.

What it is going to do is check whether the last message from the research language model contains tool calls, and if so, that means the LLM has decided to use a tool, so control is passed to the tool_node function so that the tool chosen by the LLM can be executed.

Otherwise, if the last message does not contain tool calls, it means the LLM has decided that it already has enough information, and control is passed to the compress_research function so that the research information can be compressed.

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

Researcher graphlink image 64

We already have everything needed to build the researcher agent graph.

First, we create the graph

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

Now we create the nodes that will make up the graph. They will use the functions we defined earlier.

	
< > 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;

We already have the nodes, let's connect them with edges.

  • From the START node, it proceeds to the llm_call node
  • From the llm_call node, it goes to the tool_node if the last message contains tool calls, or to the compress_research node if it does not contain tool calls* From the tool_node, it is passed to the llm_call node so that the LLM can decide whether to continue investigating or not
  • From the compress_research node, it goes to the END node
	
< > 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;

We compile the graph and represent the flow diagram

	
< > 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;

Execution of the researcher agentlink image 65

First, let's call the Scope agent to help us define the research_brief

We create a thread so we can make several calls to the agent, in case it asks us for more information

	
< > 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... │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

The agent is asking us for more information, so we invoke the agent again with the information it requests

	
< > 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.' │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

We already have the research_brief, so we can invoke the Researcher agent to begin the research.

IMPORTANT: In this case, what we do is invoke the Researcher agent with the research_brief that we already have; to do this, we pass it as the state variable researcher_messages and as the state variable research_topic

We also created a Tavily API client so we can perform internet searches.

	
< > 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/ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

As we can see, the Researcher agent has used the tavily_search tool several times to obtain information from the internet and the think_tool to think about the information it has. Finally, it has compressed the information

---

➡️ **Continue in Part 2: Multi-agent Research Supervisor**, where we will coordinate several Researcher agents in parallel.

Continue reading

Last posts -->

Have you seen these projects?

Gymnasia

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

Mobile personal training app with AI assistant, exercise library, workout tracking, diet and body measurements

Horeca chatbot

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

Chatbot conversational for cooks of hotels and restaurants. A cook, kitchen manager or room service of a hotel or restaurant can talk to the chatbot to get information about recipes and menus. But it also implements agents, with which it can edit or create new recipes or menus

View all projects -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to watch any talk?

Last talks -->

Do you want to improve with these tips?

Last tips -->

Use this locally

Hugging Face spaces allow us to run models with very simple demos, but what if the demo breaks? Or if the user deletes it? That's why I've created docker containers with some interesting spaces, to be able to use them locally, whatever happens. In fact, if you click on any project view button, it may take you to a space that doesn't work.

Flow edit

Flow edit Flow edit

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
View all containers -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to train your model with these datasets?

short-jokes-dataset

HuggingFace

Dataset with jokes in English

Use: Fine-tuning text generation models for humor

231K rows 2 columns 45 MB
View on HuggingFace →

opus100

HuggingFace

Dataset with translations from English to Spanish

Use: Training English-Spanish translation models

1M rows 2 columns 210 MB
View on HuggingFace →

netflix_titles

HuggingFace

Dataset with Netflix movies and series

Use: Netflix catalog analysis and recommendation systems

8.8K rows 12 columns 3.5 MB
View on HuggingFace →
View more datasets -->