Building Your AI Research Squad with Agno, Streamlit, and uv

Learn how to create a powerful team of specialized AI agents using Agno, Streamlit, and uv. This comprehensive guide walks you through setting up your own research assistant team that can search the web, analyze YouTube videos, crawl websites, and more!

Building Your AI Research Squad with Agno, Streamlit, and uv

Table of Contents

Remember that scene in Ocean’s Eleven where George Clooney assembles a specialized team, each member with unique skills for the perfect heist? That’s essentially what we’re doing today, except instead of breaking into casinos, we’re breaking into the world of knowledge. And instead of risking prison time, we’re just risking a higher cloud computing bill!

In the rapidly evolving AI landscape, single-purpose agents are giving way to coordinated teams of AI specialists. These teams can accomplish complex tasks that would be difficult for a single agent to handle effectively. Think of it as the difference between asking a general practitioner about a rare neurological condition versus consulting with a team of specialists. The collective intelligence always wins.

Agno, a lightweight Python library for building AI agents, makes this multi-agent approach remarkably accessible. When combined with Streamlit for beautiful interfaces and uv (a lightning-fast Python package manager), you get a toolkit that’s both powerful and practical. You can check more on Agno on: Agno get started article.

By the end of this tutorial, you’ll have a team of AI specialists that can:

  • Search the web for up-to-date information
  • Extract and analyze content from websites
  • Break down YouTube videos
  • Send professional emails
  • Explore GitHub repositories
  • Track trends on Hacker News
  • Synthesize information from all these sources

The best part? Your users will interact with this team through a clean, intuitive Streamlit interface that you can deploy anywhere.

Let’s get building!

Prerequisites and Environment Setup with uv

Before we dive into agent creation, let’s set up our development environment. We’ll use uv, the turbo-charged alternative to pip that’s up to 100x faster and built in Rust (because everything cool these days seems to be built in Rust).

Why uv?

Imagine waiting for a pizza delivery. pip is like that delivery guy who gets lost, takes wrong turns, and delivers your pizza lukewarm an hour later. uv is the delivery rocket that has your pizza at your doorstep before you even finish placing the order. It’s that fast.

Installing uv

For macOS/Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh

For Windows:

irm https://astral.sh/uv/install.ps1 | iex

Setting Up Your Project

Let’s create a fresh project and install our dependencies:

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 hackernews

Environment Variables

Our agent team needs API keys to access various services. Create a .env file in your project directory:

OPENROUTER_API_KEY=your_openrouter_key
[email protected]
[email protected]
GITHUB_ACCESS_TOKEN=your_github_token
RESEND_API_KEY=your_resend_key

You can obtain these keys from:

  • OpenRouter - For accessing various language models
  • GitHub - For GitHub repository access
  • Resend - For email capabilities

Understanding Agno Agents - Core Concepts

Before we start building our dream team, let’s understand what makes Agno agents tick. Think of Agno as the talent scout, trainer, and manager for your AI squad—it handles all the complex machinery so you can focus on creating agents with superpowers.

What Makes an Agno Agent?

At its core, an Agno agent consists of four essential components:

  1. Model: The brain of your agent. This is typically a large language model (LLM) like OpenAI’s models or, in our case, models accessed via OpenRouter like Quasar Alpha.

  2. Tools: Special abilities your agent can use to interact with the world. These range from web searches (DuckDuckGo) to sending emails (Resend) or analyzing YouTube videos.

  3. Instructions: The playbook for your agent. These are specific guidelines that shape how the agent approaches problems.

  4. Memory: The agent’s ability to remember previous interactions, which can be stored in databases like SQLite.

The Agent Lifecycle

When a user sends a query to an Agno agent, a fascinating process unfolds:

  1. Input Processing: The agent receives the user’s message.
  2. Context Assembly: The agent gathers relevant context, including its instructions and history.
  3. Tool Selection: The agent decides if and which tools to use (like searching the web).
  4. Response Generation: The LLM generates a response based on all available information.
  5. Memory Update: The interaction is stored in the agent’s memory for future reference.

This cycle happens seamlessly behind the scenes, giving users the impression of conversing with a knowledgeable entity rather than a complex piece of software.

Agent vs. Team Modes

Agno supports two primary ways to organize your AI workforce:

  1. Individual Agents: Specialized entities focused on specific tasks. Like hiring an expert consultant.

  2. Teams: Collections of agents coordinated to tackle complex tasks. Like assembling a specialized task force.

Our project will use the “coordinate” team mode, where a team leader (coordinator) breaks down complex tasks, assigns them to specialists, and synthesizes their outputs into a cohesive whole. It’s like having a project manager who knows exactly which team member to tap for each subtask.

ModeBest ForReal-World Analogy
Individual AgentFocused tasks with clear boundariesSolo consultant
Team (Coordinate)Complex tasks requiring multiple specialtiesProject team with manager

Now that we understand the foundations, let’s start building our specialized agents!

Specialized Agents - Creating Each Team Member

Now comes the fun part—assembling our dream team of AI specialists! Think of this as casting for an Ocean’s Eleven-style heist, but instead of stealing diamonds, we’re extracting knowledge. Let’s meet our crew of digital specialists, each with unique skills and a well-defined role.

The Internet Searcher - Your Web Detective

First up is our web detective, capable of finding the latest information across the internet. This agent is essential for real-time data that isn’t in our knowledge base.

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
)

Key Features:

  • DuckDuckGoTools: Our agent’s magnifying glass for investigating the web
  • add_history_to_messages: Keeps track of previous search results
  • exponential_backoff: Handles rate limits gracefully (because even digital detectives need coffee breaks)

The Web Crawler - Your Content Extractor

Next is our data extraction specialist, who can pull detailed content from specific websites when you need more than just search results.

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
)

Key Features:

  • Crawl4aiTools: A specialized tool for extracting web content
  • max_length=None: Gets the full content without truncation

The YouTube Analyst - Your Video Interpreter

Our media specialist can watch and analyze YouTube videos, extracting both captions and metadata for comprehensive insights.

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
)

Key Features:

  • YouTubeTools: Extracts captions and metadata from videos
  • Access to both what was said and video information

The Email Assistant - Your Communications Expert

Need to share findings via email? This agent handles professional communications with style and precision.

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
)

Key Features:

  • ResendTools: Professional email sending capabilities
  • Configurable sender and default recipient

The GitHub Researcher - Your Code Explorer

For technical research, our GitHub specialist can dive into repositories, pull requests, and code discussions.

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
)

Key Features:

  • GithubTools: Access to GitHub’s vast ecosystem
  • Time-aware instructions for relevance

The HackerNews Monitor - Your Tech Trend Tracker

To stay on top of tech discussions and innovations, our HackerNews specialist monitors trending stories and discussions.

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
)

Key Features:

  • HackerNewsTools: Access to the pulse of tech discussions
  • Time-aware for tracking trending topics

The Generalist - Your Synthesis Expert

Finally, our jack-of-all-trades handles general queries and synthesizes information from the specialists.

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
)

Key Features:

  • No specific tools—this agent is all about synthesis and general knowledge
  • Access to more history for comprehensive context

Common Agent Features Explained

Let’s break down some configuration options that appear across our agents:

ParameterPurposeBenefit
add_history_to_messagesIncludes chat history in contextMaintains conversation flow
num_history_responsesLimits history lengthPrevents context overflow
markdownEnables formatted outputBetter readability
exponential_backoffRetry strategy for failuresImproves reliability
add_datetime_to_instructionsAdds timestamp to instructionsTime-aware responses

With our specialized team members defined, we’re ready for the next step: bringing them together under a coordinated team structure!

Coordinating with Team Mode - Building the Whole Squad

We have our specialized agents ready to go, but they’re just individual experts without a way to collaborate. Now it’s time to bring them together under Agno’s “coordinate” team mode—think of it as appointing a project manager who knows exactly which specialist to call for each part of a complex task.

Creating the Research Team

Here’s where we define our team structure and how the agents will work together:

# --- 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()

How Team Coordination Works

Let’s break down what’s happening in this “coordinate” mode:

  1. Team Creation: We create a Team object with a collection of specialized agents as members.

  2. Coordinator Role: The team operates in “coordinate” mode, where the model specified (in our case, the same model we used for individual agents) acts as a coordinator.

  3. Task Delegation: When a user query comes in, the coordinator analyzes it and decides which specialist(s) to involve.

  4. Information Flow: The coordinator sends sub-tasks to the appropriate agents, collects their responses, and synthesizes a final answer.

  5. Memory Management: With enable_team_history=True, both the coordinator and members have access to conversation history, making follow-up questions seamless.

Team Configuration Options Explained

Let’s explore the key configuration options that make our team effective:

ParameterPurposeImpact
mode="coordinate"Sets the team operation patternCreates a hierarchical structure with a coordinator
enable_agentic_contextGives the coordinator persistent contextMaintains awareness across interactions
share_member_interactionsShares specialist outputs between membersCreates collaborative awareness
show_members_responsesControls raw output visibilitySet to False for clean final responses
enable_team_historyEnables history access for allCreates memory continuity for follow-ups

The “Success Criteria” Explained

One of the most powerful features of Agno’s team mode is the ability to define success criteria. This gives the coordinator clear guidance on when a task is considered complete:

success_criteria="The user's query has been thoroughly answered with information from all relevant specialists."

This simple statement has a profound impact—it tells the coordinator to keep working (and delegating) until it has gathered enough information from the right specialists to provide a comprehensive answer.

Think of it as setting the standard for what constitutes a “job well done” for your AI team. Without this, the coordinator might rush to conclusions or miss important specialist input.

With our team structure defined, we’re ready to create the interface that will bring this powerful AI squad to life—let’s build our Streamlit app!

Streamlit Integration - Giving Your Team a Face

Now that we have a powerful research team humming under the hood, it’s time to build an intuitive UI with Streamlit. Think of this as giving your AI Ocean’s Eleven crew a sleek command center—or at the very least, a chat window that doesn’t look like it’s from 1995.

Building the Streamlit UI

Streaming is the name of the game here—users want to see responses appearing in real-time, just like in ChatGPT or Claude. Let’s set up our Streamlit app to deliver that experience:

# --- 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)}"})

The Sidebar - Configuration and Debugging

Every great app needs a sidebar for configuration options and debugging information. Here’s how we’ve structured ours:

# --- 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)
    """)

How Streamlit and Agno Work Together

Let’s break down the integration points between Streamlit and our Agno team:

Streamlit FeaturePurposeIntegration with Agno
st.session_stateMaintains app state across interactionsStores team instance and conversation history
st.chat_messageCreates chat bubbles for conversationDisplays user queries and team responses
st.empty()Creates placeholder for streamingUpdated chunk by chunk with team’s streamed response
Sidebar componentsProvides configuration and debug optionsShows team memory and allows session reset

The magic happens in the streaming response loop. When a user submits a query:

  1. The query is added to Streamlit’s chat history
  2. It’s passed to the Agno team via team.run(query, stream=True)
  3. As chunks of the response arrive, they’re added to the placeholder, giving that satisfying real-time effect
  4. The final response is stored in session history for future context

Error Handling - When Things Go Sideways

We’ve built in robust error handling to ensure your users don’t see cryptic stack traces:

  • API key issues, rate limits, or tool failures are caught and displayed as friendly error messages
  • The team’s session remains intact, allowing users to try again with a different query
  • Debug information is available in the sidebar for troubleshooting

This resilient approach means your Streamlit app won’t crash even if one of your specialist agents encounters an issue—the show must go on!

Adding Memory and Session Management

We’ve built a powerful team and a slick UI, but there’s one more crucial ingredient: memory. Just like Ocean’s team would be pretty useless if they forgot the casino layout halfway through the heist, our AI team needs to remember previous interactions to be truly effective.

Session State: Streamlit’s Secret Weapon

Streamlit provides a built-in session state system that persists across interactions within a browser session. We’re using this to store three key elements:

  1. Team Instance: The entire research team with all its member agents
  2. Message History: All previous exchanges with the user
  3. Session ID: A unique identifier for this particular conversation

Here’s how we initialize these components:

# --- 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 = []

This simple initialization ensures that each new browser session gets a fresh team instance and message history, while maintaining continuity within the session.

Agno’s Memory Architecture

Agno provides three types of memory for our team:

  1. Chat History: The sequence of interactions between the user and the team
  2. Agentic Context: The coordinator’s understanding of the ongoing conversation
  3. Team History: Shared context across all team members

Let’s look at the memory-specific settings in our team configuration:

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 passed

The Memory Flow in Action

When a user submits a query, an elegant memory dance begins:

  1. The query is added to Streamlit’s session state messages
  2. It’s passed to the Agno team, which accesses its own history
  3. The coordinator examines the query in the context of previous interactions
  4. Individual agents receive relevant portions of the history when assigned tasks
  5. The final response is added back to session state messages

This continuous loop ensures that conversations feel natural and coherent. Ask “What was my first question?” and the team will actually know!

Balancing Memory and Performance

Memory is powerful, but it comes with a cost. We’ve implemented several optimizations to keep things running smoothly:

StrategyImplementationBenefit
Limited Historynum_of_interactions_from_history=5Prevents context overflow
Selective Displayshow_members_responses=FalseCleaner output, smaller history
Debug ToggleSidebar checkbox for memory inspectionOn-demand memory visibility
Reset Button”Clear Chat & Reset Team”Fresh start when needed

These strategies ensure our team stays quick and responsive even in long conversations.

Run the Team of Agents:

Complete Code:

Below is the complete code, you should add it in main.py file:

# 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)
    """)

Run the Team:

uv run streamlit run main.py

Troubleshooting and Best Practices

Even the best-planned heists encounter unexpected challenges, and your AI research squad is no exception. Let’s talk about some common issues and how to overcome them.

API Key Management

The most common setup issue is missing or invalid API keys. We’ve built in robust validation to catch these early:

# 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

Connection and Rate Limit Handling

When working with multiple external APIs, you’ll occasionally hit rate limits or connection issues. Our solution is the exponential_backoff parameter, which we’ve added to all our agents:

exponential_backoff=True  # Add robustness

This simple addition implements a sophisticated retry strategy that waits progressively longer between attempts, dramatically improving reliability.

Model Fallback Strategies

Depending solely on one model provider can be risky. A more resilient approach is to configure model fallbacks:

# 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"
    ]
)

This ensures that if one model is unavailable, your team gracefully switches to alternatives.

Memory Debugging

When conversation history seems off, use the debug toggle in the sidebar to inspect the team’s memory:

# 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.")

Optimizing Team Design

If your team feels sluggish or uncoordinated, consider these optimizations:

  1. Specialized Tools: Ensure each agent has only the tools it truly needs
  2. Clear Instructions: Revisit agent instructions to avoid overlapping responsibilities
  3. Success Criteria: Set specific success criteria for the team coordinator
  4. History Limits: Adjust num_of_interactions_from_history to balance context and speed
  5. Stream Responses: Always use stream=True for a more responsive user experience

Conclusion - Your AI Research Team in Action

Congratulations! You’ve just built a sophisticated AI research team that would make Danny Ocean proud. Your squad isn’t just a collection of chatbots—it’s a coordinated team of specialists that can search the web, crawl websites, analyze YouTube videos, communicate via email, explore GitHub, track tech trends, and synthesize information into cohesive responses.

Let’s recap what we’ve accomplished:

  1. Environment Setup: A lightning-fast development environment with uv
  2. Specialized Agents: A crew of AI specialists, each with unique tools and abilities
  3. Team Coordination: A sophisticated delegation system that routes tasks to the right expert
  4. Sleek UI: A responsive Streamlit interface with real-time streaming responses
  5. Memory Management: Persistent context that enables natural, ongoing conversations

What Makes This Solution Special

The power of this approach lies in its modularity and extensibility. Need another specialist? Add a new agent with the right tools. Want to switch LLM providers? Swap out OpenRouter for another model. The architecture adapts to your needs without breaking what already works.

Compared to single-agent solutions, our team approach offers:

AspectSingle AgentAgent Team
SpecializationJack of all tradesDomain experts
Tool UsageOne agent switching between toolsRight tool for each agent
Response QualityGeneric, broader knowledgeDeep expertise in specific areas
AdaptabilityLimited to one thinking patternMultiple approaches to problems

Next Steps and Expansions

Now that you have your research team up and running, here are some exciting ways to enhance it:

  1. Add More Specialists: Create agents for social media monitoring, data analysis, or language translation
  2. Persistent Database: Switch from SQLite to PostgreSQL for production-grade storage
  3. Knowledge Bases: Add vector stores to give agents specialized knowledge in their domains
  4. Custom UI: Build a branded interface with Streamlit Components or graduate to a web framework
  5. Feedback Loop: Implement user ratings to help agents improve over time

The Future of AI Teams

As AI continues to evolve, the multi-agent approach will become increasingly powerful. By building your research team with Agno and Streamlit today, you’re ahead of the curve in a rapidly advancing field. The combination of specialized knowledge, coordinated teamwork, and human-like memory creates an AI experience that feels less like a tool and more like a true research partner.

So go ahead—ask your team something complex and watch as it splits the work, gathers information, and crafts a response that draws on multiple sources of expertise. It’s not just impressive; it’s a glimpse into the future of AI assistance. Your research squad is ready for action, and the possibilities are limited only by your imagination.

Now that’s a heist worth celebrating! 🎉

Related Posts