Crea tu equipo de investigación de IA con Agno, Streamlit y uv
Aprende a crear un potente equipo de agentes de IA especializados usando Agno, Streamlit y uv.
61 min de lectura

Agno Agents Tutorials
Part 2 of 4
¿Recuerdas esa escena de Ocean’s Eleven en la que George Clooney reúne a un equipo especializado, donde cada miembro aporta habilidades únicas para el atraco perfecto? Eso es esencialmente lo que haremos hoy, salvo que en lugar de colarnos en casinos, nos colaremos en el mundo del conocimiento. ¡Y en lugar de arriesgar tiempo en prisión, solo nos arriesgamos a una factura de computación en la nube más alta!
En el panorama de la IA en rápida evolución, los agentes de propósito único están dando paso a equipos coordinados de especialistas en IA. Estos equipos pueden llevar a cabo tareas complejas que serían difíciles de gestionar con eficacia para un único agente. Piénsalo como la diferencia entre preguntar a un médico de cabecera sobre una rara enfermedad neurológica y consultar con un equipo de especialistas. La inteligencia colectiva siempre gana.
Agno, una biblioteca ligera de Python para crear agentes de IA, hace que este enfoque multiagente sea notablemente accesible. Combinada con Streamlit para crear interfaces atractivas y con uv (un gestor de paquetes de Python ultrarrápido), obtienes un conjunto de herramientas tan potente como práctico. Puedes saber más sobre Agno en el artículo primeros pasos con Agno.
Al final de este tutorial, tendrás un equipo de especialistas en IA capaz de:
- Buscar en la web información actualizada
- Extraer y analizar contenido de sitios web
- Desglosar vídeos de YouTube
- Enviar correos electrónicos profesionales
- Explorar repositorios de GitHub
- Seguir las tendencias de Hacker News
- Sintetizar información de todas estas fuentes
¿La mejor parte? Tus usuarios interactuarán con este equipo a través de una interfaz de Streamlit limpia e intuitiva que puedes desplegar en cualquier lugar.
¡Manos a la obra!
Requisitos previos y configuración del entorno con uv
Antes de sumergirnos en la creación de agentes, configuremos nuestro entorno de desarrollo. Usaremos uv, la alternativa sobrealimentada a pip, que es hasta 100 veces más rápida y está construida en Rust (porque hoy en día todo lo genial parece estar construido en Rust).
¿Por qué uv?
Imagina que esperas la entrega de una pizza. pip es como ese repartidor que se pierde, toma las calles equivocadas y te entrega la pizza fría una hora después. uv es el cohete de reparto que deja la pizza en tu puerta antes de que termines de hacer el pedido. Es así de rápido.
Instalación de uv
Para macOS/Linux:
curl -LsSf https://astral.sh/uv/install.sh | shPara Windows:
irm https://astral.sh/uv/install.ps1 | iexConfiguración del proyecto
Creemos un proyecto nuevo e instalemos las dependencias:
mkdir ai-research-team
cd ai-research-team
# Create and activate a virtual environment
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install dependencies at warp speed
uv add agno streamlit python-dotenv duckduckgo-search crawl4ai youtube-transcript-api resend pygithub hackernewsVariables de entorno
Nuestro equipo de agentes necesita claves de API para acceder a varios servicios. Crea un archivo .env en el directorio de tu proyecto:
OPENROUTER_API_KEY=your_openrouter_key
[email protected]
[email protected]
GITHUB_ACCESS_TOKEN=your_github_token
RESEND_API_KEY=your_resend_keyPuedes obtener estas claves en:
- OpenRouter - Para acceder a varios modelos de lenguaje
- GitHub - Para acceder a los repositorios de GitHub
- Resend - Para las capacidades de correo electrónico
Entendiendo los agentes de Agno: conceptos clave
Antes de empezar a construir nuestro equipo soñado, entendamos qué hace funcionar a los agentes de Agno. Piensa en Agno como el cazatalentos, el entrenador y el mánager de tu escuadrón de IA: se encarga de toda la maquinaria compleja para que tú te centres en crear agentes con superpoderes.
¿Qué compone a un agente de Agno?
En esencia, un agente de Agno consta de cuatro componentes esenciales:
-
Model: el cerebro de tu agente. Normalmente es un modelo de lenguaje grande (LLM) como los modelos de OpenAI o, en nuestro caso, modelos a los que se accede mediante OpenRouter, como Quasar Alpha.
-
Tools: habilidades especiales que tu agente puede usar para interactuar con el mundo. Van desde búsquedas en la web (DuckDuckGo) hasta el envío de correos electrónicos (Resend) o el análisis de vídeos de YouTube.
-
Instructions: el manual de juego de tu agente. Son pautas específicas que moldean cómo el agente aborda los problemas.
-
Memory: la capacidad del agente para recordar interacciones previas, que puede almacenarse en bases de datos como SQLite.
El ciclo de vida de un agente
Cuando un usuario envía una consulta a un agente de Agno, se desencadena un proceso fascinante:
- Procesamiento de la entrada: el agente recibe el mensaje del usuario.
- Composición del contexto: el agente reúne el contexto relevante, incluidas sus instrucciones y su historial.
- Selección de herramientas: el agente decide si debe usar herramientas y cuáles (como buscar en la web).
- Generación de la respuesta: el LLM genera una respuesta basada en toda la información disponible.
- Actualización de la memoria: la interacción se almacena en la memoria del agente para futuras consultas.
Este ciclo se produce de forma transparente entre bastidores, dando a los usuarios la impresión de conversar con una entidad experta en lugar de con una compleja pieza de software.
Modos de agente frente a equipo
Agno admite dos formas principales de organizar tu fuerza de trabajo de IA:
-
Agentes individuales: entidades especializadas centradas en tareas concretas. Como contratar a un consultor experto.
-
Equipos: conjuntos de agentes coordinados para abordar tareas complejas. Como reunir un grupo operativo especializado.
Nuestro proyecto utilizará el modo de equipo «coordinate», en el que un líder de equipo (coordinador) descompone las tareas complejas, las asigna a los especialistas y sintetiza sus resultados en un todo cohesivo. Es como tener un jefe de proyecto que sabe exactamente a qué miembro del equipo recurrir para cada subtarea.
| Modo | Ideal para | Analogía del mundo real |
|---|---|---|
| Agente individual | Tareas acotadas con límites claros | Consultor independiente |
| Equipo (Coordinate) | Tareas complejas que requieren varias especialidades | Equipo de proyecto con jefe |
Ahora que entendemos los fundamentos, ¡empecemos a construir nuestros agentes especializados!
Agentes especializados: creación de cada miembro del equipo
Ahora llega la parte divertida: ¡reunir nuestro equipo soñado de especialistas en IA! Piensa en esto como el casting para un atraco al estilo de Ocean’s Eleven, pero en lugar de robar diamantes, extraemos conocimiento. Conozcamos a nuestra tripulación de especialistas digitales, cada uno con habilidades únicas y un rol bien definido.
El buscador de Internet: tu detective web
El primero es nuestro detective web, capaz de encontrar la información más reciente en todo internet. Este agente es esencial para los datos en tiempo real que no están en nuestra base de conocimiento.
search_agent = Agent(
name="InternetSearcher",
model=model,
tools=[DuckDuckGoTools(search=True, news=False)],
add_history_to_messages=True,
num_history_responses=3, # Limit history passed to agent
description="Expert at finding information online.",
instructions=[
"Use duckduckgo_search for web queries.",
"Cite sources with URLs.",
"Focus on recent, reliable information."
],
add_datetime_to_instructions=True, # Add time context
markdown=True,
exponential_backoff=True # Add robustness
)Características clave:
- DuckDuckGoTools: la lupa de nuestro agente para investigar la web
- add_history_to_messages: lleva el registro de los resultados de búsqueda anteriores
- exponential_backoff: gestiona con elegancia los límites de peticiones (porque hasta los detectives digitales necesitan pausas para el café)
El rastreador web: tu extractor de contenido
El siguiente es nuestro especialista en extracción de datos, capaz de extraer contenido detallado de sitios web concretos cuando necesitas algo más que resultados de búsqueda.
crawler_agent = Agent(
name="WebCrawler",
model=model,
tools=[Crawl4aiTools(max_length=None)], # No content length limit
add_history_to_messages=True,
num_history_responses=3,
description="Extracts content from specific websites.",
instructions=[
"Use web_crawler to extract content from provided URLs.",
"Summarize key points and include the URL."
],
markdown=True,
exponential_backoff=True
)Características clave:
- Crawl4aiTools: una herramienta especializada en extraer contenido web
- max_length=None: obtiene el contenido completo sin truncarlo
El analista de YouTube: tu intérprete de vídeos
Nuestro especialista en medios puede ver y analizar vídeos de YouTube, extrayendo tanto los subtítulos como los metadatos para obtener una visión completa.
youtube_agent = Agent(
name="YouTubeAnalyst",
model=model,
tools=[YouTubeTools()],
add_history_to_messages=True,
num_history_responses=3,
description="Analyzes YouTube videos.",
instructions=[
"Extract captions and metadata for YouTube URLs.",
"Summarize key points and include the video URL."
],
markdown=True,
exponential_backoff=True
)Características clave:
- YouTubeTools: extrae subtítulos y metadatos de los vídeos
- Acceso tanto a lo que se dijo como a la información del vídeo
El asistente de correo: tu experto en comunicación
¿Necesitas compartir hallazgos por correo electrónico? Este agente gestiona las comunicaciones profesionales con estilo y precisión.
email_agent = Agent(
name="EmailAssistant",
model=model,
tools=[ResendTools(from_email=EMAIL_FROM, api_key=RESEND_API_KEY)],
add_history_to_messages=True,
num_history_responses=3,
description="Sends emails professionally.",
instructions=[
"send professional emails based on context or user request.",
f"Default recipient is {EMAIL_TO}, but use recipient specified in the query if provided.",
"Include URLs and links clearly.",
"Ensure the tone is professional and courteous."
],
markdown=True,
exponential_backoff=True
)Características clave:
- ResendTools: capacidad de envío de correos electrónicos profesionales
- Remitente y destinatario predeterminado configurables
El investigador de GitHub: tu explorador de código
Para la investigación técnica, nuestro especialista en GitHub puede sumergirse en repositorios, pull requests y discusiones sobre código.
github_agent = Agent(
name="GitHubResearcher",
model=model,
tools=[GithubTools(access_token=GITHUB_ACCESS_TOKEN)],
add_history_to_messages=True,
num_history_responses=3,
description="Explores GitHub repositories.",
instructions=[
"Search repositories or list pull requests based on user query.",
"Include repository URLs and summarize findings concisely."
],
markdown=True,
exponential_backoff=True,
add_datetime_to_instructions=True
)Características clave:
- GithubTools: acceso al vasto ecosistema de GitHub
- Instrucciones con conciencia temporal para mayor relevancia
El monitor de Hacker News: tu rastreador de tendencias tecnológicas
Para mantenernos al tanto de las discusiones e innovaciones tecnológicas, nuestro especialista en Hacker News monitoriza las historias y los debates en tendencia.
hackernews_agent = Agent(
name="HackerNewsMonitor",
model=model,
tools=[HackerNewsTools()],
add_history_to_messages=True,
num_history_responses=3,
description="Tracks Hacker News trends.",
instructions=[
"Fetch top stories using get_top_hackernews_stories.",
"Summarize discussions and include story URLs."
],
markdown=True,
exponential_backoff=True,
add_datetime_to_instructions=True
)Características clave:
- HackerNewsTools: acceso al pulso de las discusiones tecnológicas
- Conciencia temporal para seguir los temas en tendencia
El generalista: tu experto en síntesis
Por último, nuestro comodín se encarga de las consultas generales y sintetiza la información de los especialistas.
general_agent = Agent(
name="GeneralAssistant",
model=model,
add_history_to_messages=True,
num_history_responses=5, # More history for context
description="Handles general queries and synthesizes information from specialists.",
instructions=[
"Answer general questions or combine specialist inputs.",
"If specialists provide information, synthesize it clearly.",
"If a query doesn't fit other specialists, attempt to answer directly.",
"Maintain a professional tone."
],
markdown=True,
exponential_backoff=True
)Características clave:
- Sin herramientas específicas: este agente se centra en la síntesis y el conocimiento general
- Acceso a un mayor historial para un contexto más completo
Explicación de las funciones comunes de los agentes
Analicemos algunas opciones de configuración que se repiten en nuestros agentes:
| Parámetro | Propósito | Beneficio |
|---|---|---|
add_history_to_messages |
Incluye el historial de chat en el contexto | Mantiene el hilo de la conversación |
num_history_responses |
Limita la longitud del historial | Evita el desbordamiento del contexto |
markdown |
Habilita la salida con formato | Mejor legibilidad |
exponential_backoff |
Estrategia de reintento ante fallos | Mejora la fiabilidad |
add_datetime_to_instructions |
Añade la fecha y la hora a las instrucciones | Respuestas con conciencia temporal |
Con los miembros especializados de nuestro equipo definidos, estamos listos para el siguiente paso: ¡reunirlos bajo una estructura de equipo coordinado!
Coordinación con el modo de equipo: construir todo el escuadrón
Ya tenemos nuestros agentes especializados listos, pero son solo expertos individuales sin una forma de colaborar. Ahora es el momento de reunirlos bajo el modo de equipo «coordinate» de Agno; piénsalo como nombrar a un jefe de proyecto que sabe exactamente a qué especialista llamar para cada parte de una tarea compleja.
Creación del equipo de investigación
Aquí es donde definimos la estructura de nuestro equipo y cómo trabajarán juntos los agentes:
# --- Team Initialization (in Session State) ---
def initialize_team():
"""Initializes or re-initializes the research team."""
return Team(
name="ResearchAssistantTeam",
mode="coordinate",
model=model,
members=[
search_agent,
crawler_agent,
youtube_agent,
email_agent,
github_agent,
hackernews_agent,
general_agent
],
description="Coordinates specialists to handle research tasks.",
instructions=[
"Analyze the query and assign tasks to specialists.",
"Delegate based on task type:",
"- Web searches: InternetSearcher",
"- URL content: WebCrawler",
"- YouTube videos: YouTubeAnalyst",
"- Emails: EmailAssistant",
"- GitHub queries: GitHubResearcher",
"- Hacker News: HackerNewsMonitor",
"- General or synthesis: GeneralAssistant",
"Synthesize responses into a cohesive answer.",
"Cite sources and maintain clarity.",
"Always check previous conversations in memory before responding.",
"When asked about previous information or to recall something mentioned before, refer to your memory of past interactions.",
"Use all relevant information from memory when answering follow-up questions."
],
success_criteria="The user's query has been thoroughly answered with information from all relevant specialists.",
enable_agentic_context=True, # Coordinator maintains context
share_member_interactions=True, # Members see previous member interactions in context
show_members_responses=False, # Don't show raw member responses in final output
markdown=True,
show_tool_calls=False, # Don't show raw tool calls in final output
enable_team_history=True, # Pass history between coordinator/members
num_of_interactions_from_history=5 # Limit history passed
)
if "team" not in st.session_state:
st.session_state.team = initialize_team()Cómo funciona la coordinación del equipo
Analicemos qué ocurre en este modo «coordinate»:
-
Creación del equipo: creamos un objeto
Teamcon una colección de agentes especializados como miembros. -
Rol del coordinador: el equipo funciona en modo «coordinate», donde el modelo especificado (en nuestro caso, el mismo
modelque usamos para los agentes individuales) actúa como coordinador. -
Delegación de tareas: cuando llega una consulta del usuario, el coordinador la analiza y decide a qué especialista o especialistas involucrar.
-
Flujo de información: el coordinador envía subtareas a los agentes apropiados, recopila sus respuestas y sintetiza una respuesta final.
-
Gestión de la memoria: con
enable_team_history=True, tanto el coordinador como los miembros tienen acceso al historial de la conversación, lo que hace que las preguntas de seguimiento sean fluidas.
Explicación de las opciones de configuración del equipo
Exploremos las opciones de configuración clave que hacen que nuestro equipo sea eficaz:
| Parámetro | Propósito | Impacto |
|---|---|---|
mode="coordinate" |
Establece el patrón de funcionamiento del equipo | Crea una estructura jerárquica con un coordinador |
enable_agentic_context |
Da al coordinador un contexto persistente | Mantiene la conciencia a lo largo de las interacciones |
share_member_interactions |
Comparte los resultados de los especialistas entre los miembros | Crea conciencia colaborativa |
show_members_responses |
Controla la visibilidad de las respuestas sin procesar | Se establece en False para respuestas finales limpias |
enable_team_history |
Habilita el acceso al historial para todos | Crea continuidad de memoria para las preguntas de seguimiento |
Explicación de los «criterios de éxito»
Una de las funciones más potentes del modo de equipo de Agno es la posibilidad de definir criterios de éxito. Esto da al coordinador una guía clara sobre cuándo se considera completada una tarea:
success_criteria="The user's query has been thoroughly answered with information from all relevant specialists."Esta simple declaración tiene un impacto profundo: le indica al coordinador que siga trabajando (y delegando) hasta reunir suficiente información de los especialistas adecuados para ofrecer una respuesta completa.
Piénsalo como establecer el estándar de lo que constituye un «trabajo bien hecho» para tu equipo de IA. Sin esto, el coordinador podría precipitarse hacia conclusiones o pasar por alto aportaciones importantes de los especialistas.
Con la estructura de nuestro equipo definida, estamos listos para crear la interfaz que dará vida a este potente escuadrón de IA: ¡construyamos nuestra aplicación de Streamlit!
Integración con Streamlit: dale una cara a tu equipo
Ahora que tenemos un potente equipo de investigación rugiendo bajo el capó, es hora de construir una interfaz de usuario intuitiva con Streamlit. Piensa en esto como darle a tu equipo de IA estilo Ocean’s Eleven un elegante centro de mando, o al menos una ventana de chat que no parezca sacada de 1995.
Construcción de la interfaz de Streamlit
El streaming es la clave aquí: los usuarios quieren ver las respuestas aparecer en tiempo real, igual que en ChatGPT o Claude. Configuremos nuestra app de Streamlit para ofrecer esa experiencia:
# --- Streamlit UI ---
st.title("🤖 Research Assistant Team")
st.markdown("""
This team coordinates specialists to assist with:
- 🔍 Web searches
- 🌐 Website content extraction
- 📺 YouTube video analysis
- 📧 Email drafting/sending
- 💻 GitHub repository exploration
- 📰 Hacker News trends
- 🧠 General queries and synthesis
""")
# Display chat messages from history
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Handle user input
user_query = st.chat_input("Ask the research team anything...")
if user_query:
# Add user message to chat history
st.session_state.messages.append({"role": "user", "content": user_query})
# Display user message
with st.chat_message("user"):
st.markdown(user_query)
# Display team response (Streaming)
with st.chat_message("assistant"):
message_placeholder = st.empty()
full_response = ""
try:
# Use stream=True for the team run
response_stream: Iterator[RunResponse] = st.session_state.team.run(user_query, stream=True) # Ensure type hint
for chunk in response_stream:
# Check if content is present and a string
if chunk.content and isinstance(chunk.content, str):
full_response += chunk.content
message_placeholder.markdown(full_response + "▌") # Add cursor effect
message_placeholder.markdown(full_response) # Final response without cursor
# Update memory debug information for display
if hasattr(st.session_state.team, 'memory') and hasattr(st.session_state.team.memory, 'messages'):
try:
# Extract only role and content safely
st.session_state.memory_dump = [
{"role": m.role if hasattr(m, 'role') else 'unknown',
"content": m.content if hasattr(m, 'content') else str(m)}
for m in st.session_state.team.memory.messages
]
except Exception as e:
st.session_state.memory_dump = f"Error accessing memory messages: {str(e)}"
else:
st.session_state.memory_dump = "Team memory object or messages not found/accessible."
# Add the final assistant response to Streamlit's chat history
st.session_state.messages.append({"role": "assistant", "content": full_response})
except Exception as e:
st.exception(e) # Show full traceback in Streamlit console for debugging
error_message = f"An error occurred: {str(e)}\n\nPlease check your API keys and tool configurations. Try rephrasing your query."
st.error(error_message)
message_placeholder.markdown(f"⚠️ {error_message}")
# Add error message to history for context
st.session_state.messages.append({"role": "assistant", "content": f"Error: {str(e)}"})La barra lateral: configuración y depuración
Toda gran aplicación necesita una barra lateral para las opciones de configuración y la información de depuración. Así es como hemos estructurado la nuestra:
# --- Sidebar ---
with st.sidebar:
st.title("Team Settings")
# Memory debug section
if st.checkbox("Show Team Memory Contents", value=False):
st.subheader("Team Memory Contents (Debug)")
if "memory_dump" in st.session_state:
try:
# Use pformat for potentially complex structures
memory_str = pformat(st.session_state.memory_dump, indent=2, width=80)
st.code(memory_str, language="python")
except Exception as format_e:
st.warning(f"Could not format memory dump: {format_e}")
st.json(st.session_state.memory_dump) # Fallback to json
else:
st.info("No memory contents to display yet. Interact with the team first.")
st.markdown(f"**Session ID**: `{st.session_state.team_session_id}`")
st.markdown(f"**Model**: {model_name}")
# Memory information
st.subheader("Team Memory")
st.markdown("This team remembers conversations within this browser session. Clearing the chat resets the memory.")
# Clear chat button
if st.button("Clear Chat & Reset Team"):
st.session_state.messages = []
st.session_state.team_session_id = f"streamlit-team-session-{int(time.time())}" # New ID for clarity
st.session_state.team = initialize_team() # Re-initialize the team to reset its state
if "memory_dump" in st.session_state:
del st.session_state.memory_dump # Clear the dump
st.rerun()
st.title("About")
st.markdown("""
**How it works**:
- The team coordinator analyzes your query.
- Tasks are delegated to specialists (Searcher, Crawler, YouTube Analyst, Email, GitHub, HackerNews, General).
- Responses are synthesized into a final answer.
- Team memory retains context within this session.
**Example queries**:
- "What are the latest AI breakthroughs?"
- "Crawl agno.com and summarize the homepage."
- "Summarize the YouTube video: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
- "Draft an email to [email protected] introducing our research services."
- "Find popular AI repositories on GitHub created in the last month."
- "What's trending on Hacker News today?"
- "What was the first question I asked you?" (tests memory)
""")Cómo trabajan juntos Streamlit y Agno
Analicemos los puntos de integración entre Streamlit y nuestro equipo de Agno:
| Función de Streamlit | Propósito | Integración con Agno |
|---|---|---|
st.session_state |
Mantiene el estado de la app entre interacciones | Almacena la instancia del equipo y el historial de la conversación |
st.chat_message |
Crea burbujas de chat para la conversación | Muestra las consultas del usuario y las respuestas del equipo |
st.empty() |
Crea un marcador de posición para el streaming | Se actualiza fragmento a fragmento con la respuesta transmitida del equipo |
| Componentes de la barra lateral | Ofrecen opciones de configuración y depuración | Muestran la memoria del equipo y permiten reiniciar la sesión |
La magia ocurre en el bucle de respuesta en streaming. Cuando un usuario envía una consulta:
- La consulta se añade al historial de chat de Streamlit
- Se pasa al equipo de Agno mediante
team.run(query, stream=True) - A medida que llegan los fragmentos de la respuesta, se añaden al marcador de posición, logrando ese satisfactorio efecto en tiempo real
- La respuesta final se almacena en el historial de la sesión para el contexto futuro
Manejo de errores: cuando las cosas se tuercen
Hemos incorporado un manejo de errores robusto para que tus usuarios no vean trazas de error crípticas:
- Los problemas con las claves de API, los límites de peticiones o los fallos de las herramientas se capturan y se muestran como mensajes de error amigables
- La sesión del equipo permanece intacta, lo que permite a los usuarios volver a intentarlo con una consulta diferente
- La información de depuración está disponible en la barra lateral para la resolución de problemas
Este enfoque resistente significa que tu app de Streamlit no se bloqueará aunque uno de tus agentes especializados tenga un problema: ¡el espectáculo debe continuar!
Añadir memoria y gestión de sesiones
Hemos construido un equipo potente y una interfaz elegante, pero falta un ingrediente crucial: la memoria. Igual que el equipo de Ocean sería bastante inútil si olvidara el plano del casino a mitad del atraco, nuestro equipo de IA necesita recordar las interacciones previas para ser realmente eficaz.
Session state: el arma secreta de Streamlit
Streamlit ofrece un sistema de estado de sesión integrado que persiste entre las interacciones dentro de una sesión del navegador. Lo usamos para almacenar tres elementos clave:
- La instancia del equipo: el equipo de investigación completo con todos sus agentes miembros
- El historial de mensajes: todos los intercambios previos con el usuario
- El ID de sesión: un identificador único para esta conversación concreta
Así es como inicializamos estos componentes:
# --- Session State Initialization ---
# Initialize team_session_id for this specific browser session
if "team_session_id" not in st.session_state:
st.session_state.team_session_id = f"streamlit-team-session-{int(time.time())}"
# Initialize chat message history
if "messages" not in st.session_state:
st.session_state.messages = []Esta sencilla inicialización garantiza que cada nueva sesión del navegador obtenga una instancia nueva del equipo y un historial de mensajes limpio, manteniendo al mismo tiempo la continuidad dentro de la sesión.
La arquitectura de memoria de Agno
Agno ofrece tres tipos de memoria para nuestro equipo:
- Historial de chat: la secuencia de interacciones entre el usuario y el equipo
- Contexto agéntico: la comprensión que tiene el coordinador de la conversación en curso
- Historial del equipo: contexto compartido entre todos los miembros del equipo
Veamos los ajustes específicos de memoria en la configuración de nuestro equipo:
enable_agentic_context=True, # Coordinator maintains context
share_member_interactions=True, # Members see previous member interactions
enable_team_history=True, # Pass history between coordinator/members
num_of_interactions_from_history=5 # Limit history passedEl flujo de memoria en acción
Cuando un usuario envía una consulta, comienza una elegante danza de memoria:
- La consulta se añade a los mensajes del estado de sesión de Streamlit
- Se pasa al equipo de Agno, que accede a su propio historial
- El coordinador examina la consulta en el contexto de las interacciones previas
- Los agentes individuales reciben las partes relevantes del historial cuando se les asignan tareas
- La respuesta final se añade de nuevo a los mensajes del estado de sesión
Este bucle continuo garantiza que las conversaciones resulten naturales y coherentes. ¡Pregunta «¿cuál fue mi primera pregunta?» y el equipo lo sabrá de verdad!
Equilibrio entre memoria y rendimiento
La memoria es poderosa, pero tiene un coste. Hemos implementado varias optimizaciones para que todo funcione con fluidez:
| Estrategia | Implementación | Beneficio |
|---|---|---|
| Historial limitado | num_of_interactions_from_history=5 |
Evita el desbordamiento del contexto |
| Visualización selectiva | show_members_responses=False |
Salida más limpia e historial más pequeño |
| Interruptor de depuración | Casilla en la barra lateral para inspeccionar la memoria | Visibilidad de la memoria bajo demanda |
| Botón de reinicio | “Clear Chat & Reset Team” | Un nuevo comienzo cuando se necesita |
Estas estrategias garantizan que nuestro equipo siga siendo rápido y receptivo incluso en conversaciones largas.
Ejecuta el equipo de agentes:
Código completo:
A continuación tienes el código completo; debes añadirlo en el archivo main.py:
# app.py
import os
import streamlit as st
from dotenv import load_dotenv
import time
from pprint import pformat
from typing import Iterator # Added for type hinting
# Agno Imports
from agno.agent import Agent
from agno.models.openrouter import OpenRouter
from agno.team import Team
from agno.run.response import RunResponse # Added for type hinting
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.crawl4ai import Crawl4aiTools
from agno.tools.youtube import YouTubeTools
from agno.tools.resend import ResendTools
from agno.tools.github import GithubTools
from agno.tools.hackernews import HackerNewsTools
# --- Configuration ---
# Load environment variables from .env file
load_dotenv()
# Check for essential API keys
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
EMAIL_FROM = os.getenv("EMAIL_FROM")
EMAIL_TO = os.getenv("EMAIL_TO") # Default recipient
GITHUB_ACCESS_TOKEN = os.getenv("GITHUB_ACCESS_TOKEN")
RESEND_API_KEY = os.getenv("RESEND_API_KEY") # ResendTools requires this
# Simple validation for required keys
required_keys = {
"OPENROUTER_API_KEY": OPENROUTER_API_KEY,
"EMAIL_FROM": EMAIL_FROM,
"EMAIL_TO": EMAIL_TO,
"GITHUB_ACCESS_TOKEN": GITHUB_ACCESS_TOKEN,
"RESEND_API_KEY": RESEND_API_KEY
}
missing_keys = [name for name, key in required_keys.items() if not key]
if missing_keys:
st.error(f"Missing required environment variables: {', '.join(missing_keys)}. Please set them in your .env file or system environment.")
st.stop() # Stop execution if keys are missing
# Set Streamlit page configuration
st.set_page_config(
page_title="Research Assistant Team",
page_icon="🧠",
layout="wide"
)
# --- Model Initialization ---
# Initialize OpenRouter model only, no fallback
try:
model = OpenRouter(id="openrouter/optimus-alpha", api_key=OPENROUTER_API_KEY)
model_name = "OpenRouter (openrouter/optimus-alpha)"
st.sidebar.info(f"Using model: {model_name}")
except Exception as e:
st.error(f"Failed to initialize OpenRouter model: {e}")
st.stop()
# --- Session State Initialization ---
# Initialize team_session_id for this specific browser session
if "team_session_id" not in st.session_state:
st.session_state.team_session_id = f"streamlit-team-session-{int(time.time())}"
# Initialize chat message history
if "messages" not in st.session_state:
st.session_state.messages = []
# --- Agent Definitions ---
# Define specialized agents
search_agent = Agent(
name="InternetSearcher",
model=model,
tools=[DuckDuckGoTools(search=True, news=False)],
add_history_to_messages=True,
num_history_responses=3, # Limit history passed to agent
description="Expert at finding information online.",
instructions=[
"Use duckduckgo_search for web queries.",
"Cite sources with URLs.",
"Focus on recent, reliable information."
],
add_datetime_to_instructions=True, # Add time context
markdown=True,
exponential_backoff=True # Add robustness
)
crawler_agent = Agent(
name="WebCrawler",
model=model,
tools=[Crawl4aiTools(max_length=None)], # Consider setting a sensible max_length
add_history_to_messages=True,
num_history_responses=3,
description="Extracts content from specific websites.",
instructions=[
"Use web_crawler to extract content from provided URLs.",
"Summarize key points and include the URL."
],
markdown=True,
exponential_backoff=True
)
youtube_agent = Agent(
name="YouTubeAnalyst",
model=model,
tools=[YouTubeTools()],
add_history_to_messages=True,
num_history_responses=3,
description="Analyzes YouTube videos.",
instructions=[
"Extract captions and metadata for YouTube URLs.",
"Summarize key points and include the video URL."
],
markdown=True,
exponential_backoff=True
)
email_agent = Agent(
name="EmailAssistant",
model=model,
tools=[ResendTools(from_email=EMAIL_FROM, api_key=RESEND_API_KEY)], # Pass required args
add_history_to_messages=True,
num_history_responses=3,
description="Sends emails professionally.",
instructions=[
"send professional emails based on context or user request.",
f"Default recipient is {EMAIL_TO}, but use recipient specified in the query if provided.",
"Include URLs and links clearly.",
"Ensure the tone is professional and courteous."
],
markdown=True,
exponential_backoff=True
)
github_agent = Agent(
name="GitHubResearcher",
model=model,
tools=[GithubTools(access_token=GITHUB_ACCESS_TOKEN)], # Pass required args
add_history_to_messages=True,
num_history_responses=3,
description="Explores GitHub repositories.",
instructions=[
"Search repositories or list pull requests based on user query.",
"Include repository URLs and summarize findings concisely."
],
markdown=True,
exponential_backoff=True,
add_datetime_to_instructions=True
)
hackernews_agent = Agent(
name="HackerNewsMonitor",
model=model,
tools=[HackerNewsTools()],
add_history_to_messages=True,
num_history_responses=3,
description="Tracks Hacker News trends.",
instructions=[
"Fetch top stories using get_top_hackernews_stories.",
"Summarize discussions and include story URLs."
],
markdown=True,
exponential_backoff=True,
add_datetime_to_instructions=True
)
# Generalist Agent (No KB in this version)
general_agent = Agent(
name="GeneralAssistant",
model=model,
add_history_to_messages=True,
num_history_responses=5, # Can access slightly more history
description="Handles general queries and synthesizes information from specialists.",
instructions=[
"Answer general questions or combine specialist inputs.",
"If specialists provide information, synthesize it clearly.",
"If a query doesn't fit other specialists, attempt to answer directly.",
"Maintain a professional tone."
],
markdown=True,
exponential_backoff=True
)
# --- Team Initialization (in Session State) ---
def initialize_team():
"""Initializes or re-initializes the research team."""
return Team(
name="ResearchAssistantTeam",
mode="coordinate",
model=model,
members=[
search_agent,
crawler_agent,
youtube_agent,
email_agent,
github_agent,
hackernews_agent,
general_agent
],
description="Coordinates specialists to handle research tasks.",
instructions=[
"Analyze the query and assign tasks to specialists.",
"Delegate based on task type:",
"- Web searches: InternetSearcher",
"- URL content: WebCrawler",
"- YouTube videos: YouTubeAnalyst",
"- Emails: EmailAssistant",
"- GitHub queries: GitHubResearcher",
"- Hacker News: HackerNewsMonitor",
"- General or synthesis: GeneralAssistant",
"Synthesize responses into a cohesive answer.",
"Cite sources and maintain clarity.",
"Always check previous conversations in memory before responding.",
"When asked about previous information or to recall something mentioned before, refer to your memory of past interactions.",
"Use all relevant information from memory when answering follow-up questions."
],
success_criteria="The user's query has been thoroughly answered with information from all relevant specialists.",
enable_agentic_context=True, # Coordinator maintains context
share_member_interactions=True, # Members see previous member interactions in context
show_members_responses=False, # Don't show raw member responses in final output
markdown=True,
show_tool_calls=False, # Don't show raw tool calls in final output
enable_team_history=True, # Pass history between coordinator/members
num_of_interactions_from_history=5 # Limit history passed
)
if "team" not in st.session_state:
st.session_state.team = initialize_team()
# --- Streamlit UI ---
st.title("🤖 Research Assistant Team")
st.markdown("""
This team coordinates specialists to assist with:
- 🔍 Web searches
- 🌐 Website content extraction
- 📺 YouTube video analysis
- 📧 Email drafting/sending
- 💻 GitHub repository exploration
- 📰 Hacker News trends
- 🧠 General queries and synthesis
""")
# Display chat messages from history
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Handle user input
user_query = st.chat_input("Ask the research team anything...")
if user_query:
# Add user message to chat history
st.session_state.messages.append({"role": "user", "content": user_query})
# Display user message
with st.chat_message("user"):
st.markdown(user_query)
# Display team response (Streaming)
with st.chat_message("assistant"):
message_placeholder = st.empty()
full_response = ""
try:
# Use stream=True for the team run
response_stream: Iterator[RunResponse] = st.session_state.team.run(user_query, stream=True) # Ensure type hint
for chunk in response_stream:
# Check if content is present and a string
if chunk.content and isinstance(chunk.content, str):
full_response += chunk.content
message_placeholder.markdown(full_response + "▌") # Add cursor effect
message_placeholder.markdown(full_response) # Final response without cursor
# Update memory debug information for display
if hasattr(st.session_state.team, 'memory') and hasattr(st.session_state.team.memory, 'messages'):
try:
# Extract only role and content safely
st.session_state.memory_dump = [
{"role": m.role if hasattr(m, 'role') else 'unknown',
"content": m.content if hasattr(m, 'content') else str(m)}
for m in st.session_state.team.memory.messages
]
except Exception as e:
st.session_state.memory_dump = f"Error accessing memory messages: {str(e)}"
else:
st.session_state.memory_dump = "Team memory object or messages not found/accessible."
# Add the final assistant response to Streamlit's chat history
st.session_state.messages.append({"role": "assistant", "content": full_response})
except Exception as e:
st.exception(e) # Show full traceback in Streamlit console for debugging
error_message = f"An error occurred: {str(e)}\n\nPlease check your API keys and tool configurations. Try rephrasing your query."
st.error(error_message)
message_placeholder.markdown(f"⚠️ {error_message}")
# Add error message to history for context
st.session_state.messages.append({"role": "assistant", "content": f"Error: {str(e)}"})
# --- Sidebar ---
with st.sidebar:
st.title("Team Settings")
# Memory debug section
if st.checkbox("Show Team Memory Contents", value=False):
st.subheader("Team Memory Contents (Debug)")
if "memory_dump" in st.session_state:
try:
# Use pformat for potentially complex structures
memory_str = pformat(st.session_state.memory_dump, indent=2, width=80)
st.code(memory_str, language="python")
except Exception as format_e:
st.warning(f"Could not format memory dump: {format_e}")
st.json(st.session_state.memory_dump) # Fallback to json
else:
st.info("No memory contents to display yet. Interact with the team first.")
st.markdown(f"**Session ID**: `{st.session_state.team_session_id}`")
st.markdown(f"**Model**: {model_name}")
# Memory information
st.subheader("Team Memory")
st.markdown("This team remembers conversations within this browser session. Clearing the chat resets the memory.")
# Clear chat button
if st.button("Clear Chat & Reset Team"):
st.session_state.messages = []
st.session_state.team_session_id = f"streamlit-team-session-{int(time.time())}" # New ID for clarity
st.session_state.team = initialize_team() # Re-initialize the team to reset its state
if "memory_dump" in st.session_state:
del st.session_state.memory_dump # Clear the dump
st.rerun()
st.title("About")
st.markdown("""
**How it works**:
- The team coordinator analyzes your query.
- Tasks are delegated to specialists (Searcher, Crawler, YouTube Analyst, Email, GitHub, HackerNews, General).
- Responses are synthesized into a final answer.
- Team memory retains context within this session.
**Example queries**:
- "What are the latest AI breakthroughs?"
- "Crawl agno.com and summarize the homepage."
- "Summarize the YouTube video: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
- "Draft an email to [email protected] introducing our research services."
- "Find popular AI repositories on GitHub created in the last month."
- "What's trending on Hacker News today?"
- "What was the first question I asked you?" (tests memory)
""")Ejecutar el equipo:
uv run streamlit run main.pySolución de problemas y buenas prácticas
Incluso los atracos mejor planificados se encuentran con desafíos inesperados, y tu escuadrón de investigación de IA no es una excepción. Hablemos de algunos problemas comunes y de cómo superarlos.
Gestión de claves de API
El problema de configuración más común son las claves de API ausentes o no válidas. Hemos incorporado una validación robusta para detectarlas pronto:
# Simple validation for required keys
required_keys = {
"OPENROUTER_API_KEY": OPENROUTER_API_KEY,
"EMAIL_FROM": EMAIL_FROM,
"EMAIL_TO": EMAIL_TO,
"GITHUB_ACCESS_TOKEN": GITHUB_ACCESS_TOKEN,
"RESEND_API_KEY": RESEND_API_KEY
}
missing_keys = [name for name, key in required_keys.items() if not key]
if missing_keys:
st.error(f"Missing required environment variables: {', '.join(missing_keys)}. Please set them in your .env file or system environment.")
st.stop() # Stop execution if keys are missingManejo de conexiones y límites de peticiones
Al trabajar con múltiples API externas, ocasionalmente te toparás con límites de peticiones o problemas de conexión. Nuestra solución es el parámetro exponential_backoff, que hemos añadido a todos nuestros agentes:
exponential_backoff=True # Add robustnessEsta simple adición implementa una sofisticada estrategia de reintentos que espera progresivamente más entre intentos, lo que mejora drásticamente la fiabilidad.
Estrategias de respaldo de modelos
Depender únicamente de un proveedor de modelos puede ser arriesgado. Un enfoque más resistente es configurar modelos de respaldo:
# Alternative implementation (not in current code)
model = OpenRouter(
id="openrouter/optimus-alpha",
api_key=OPENROUTER_API_KEY,
fallback_models=[
"openai/gpt-4-turbo",
"anthropic/claude-3-opus"
]
)Esto garantiza que, si un modelo no está disponible, tu equipo cambie con elegancia a las alternativas.
Depuración de la memoria
Cuando el historial de la conversación parezca extraño, usa el interruptor de depuración de la barra lateral para inspeccionar la memoria del equipo:
# Memory debug section
if st.checkbox("Show Team Memory Contents", value=False):
st.subheader("Team Memory Contents (Debug)")
if "memory_dump" in st.session_state:
try:
# Use pformat for potentially complex structures
memory_str = pformat(st.session_state.memory_dump, indent=2, width=80)
st.code(memory_str, language="python")
except Exception as format_e:
st.warning(f"Could not format memory dump: {format_e}")
st.json(st.session_state.memory_dump) # Fallback to json
else:
st.info("No memory contents to display yet. Interact with the team first.")Optimización del diseño del equipo
Si tu equipo parece lento o descoordinado, considera estas optimizaciones:
- Herramientas especializadas: asegúrate de que cada agente tenga solo las herramientas que realmente necesita
- Instrucciones claras: revisa las instrucciones de los agentes para evitar responsabilidades solapadas
- Criterios de éxito: establece criterios de éxito específicos para el coordinador del equipo
- Límites de historial: ajusta
num_of_interactions_from_historypara equilibrar contexto y velocidad - Respuestas en streaming: usa siempre
stream=Truepara una experiencia de usuario más receptiva
Conclusión: tu equipo de investigación de IA en acción
¡Enhorabuena! Acabas de construir un sofisticado equipo de investigación de IA del que Danny Ocean estaría orgulloso. Tu escuadrón no es solo una colección de chatbots: es un equipo coordinado de especialistas que puede buscar en la web, rastrear sitios web, analizar vídeos de YouTube, comunicarse por correo electrónico, explorar GitHub, seguir tendencias tecnológicas y sintetizar información en respuestas cohesionadas.
Recapitulemos lo que hemos logrado:
- Configuración del entorno: un entorno de desarrollo ultrarrápido con
uv - Agentes especializados: una tripulación de especialistas en IA, cada uno con herramientas y habilidades únicas
- Coordinación del equipo: un sofisticado sistema de delegación que dirige las tareas al experto adecuado
- Interfaz elegante: una interfaz de Streamlit receptiva con respuestas en streaming en tiempo real
- Gestión de la memoria: contexto persistente que permite conversaciones naturales y continuas
Qué hace especial a esta solución
La fuerza de este enfoque reside en su modularidad y extensibilidad. ¿Necesitas otro especialista? Añade un nuevo agente con las herramientas adecuadas. ¿Quieres cambiar de proveedor de LLM? Sustituye OpenRouter por otro modelo. La arquitectura se adapta a tus necesidades sin romper lo que ya funciona.
En comparación con las soluciones de un solo agente, nuestro enfoque de equipo ofrece:
| Aspecto | Agente único | Equipo de agentes |
|---|---|---|
| Especialización | Todólogo | Expertos en cada dominio |
| Uso de herramientas | Un agente que alterna entre herramientas | La herramienta adecuada para cada agente |
| Calidad de las respuestas | Conocimiento genérico y más amplio | Experiencia profunda en áreas concretas |
| Adaptabilidad | Limitado a un único patrón de pensamiento | Múltiples enfoques para los problemas |
Próximos pasos y ampliaciones
Ahora que tienes tu equipo de investigación en marcha, aquí tienes algunas formas interesantes de mejorarlo:
- Añade más especialistas: crea agentes para monitorizar redes sociales, analizar datos o traducir idiomas
- Base de datos persistente: cambia de SQLite a PostgreSQL para un almacenamiento de nivel de producción
- Bases de conocimiento: añade almacenes de vectores para dar a los agentes conocimiento especializado en sus dominios
- Interfaz personalizada: crea una interfaz con tu marca usando Streamlit Components o da el salto a un framework web
- Bucle de retroalimentación: implementa valoraciones de los usuarios para ayudar a los agentes a mejorar con el tiempo
El futuro de los equipos de IA
A medida que la IA sigue evolucionando, el enfoque multiagente será cada vez más potente. Al construir hoy tu equipo de investigación con Agno y Streamlit, te adelantas a la curva en un campo que avanza rápidamente. La combinación de conocimiento especializado, trabajo en equipo coordinado y una memoria similar a la humana crea una experiencia de IA que se siente menos como una herramienta y más como un auténtico socio de investigación.
Así que adelante: hazle a tu equipo una pregunta compleja y observa cómo divide el trabajo, recopila información y elabora una respuesta que aprovecha múltiples fuentes de experiencia. No es solo impresionante; es un vistazo al futuro de la asistencia de IA. Tu escuadrón de investigación está listo para la acción, y las posibilidades solo están limitadas por tu imaginación.
¡Ahora sí, ese es un atraco que vale la pena celebrar! 🎉


