diff --git a/cookbook/examples/streamlit_apps/gtm_outreach/README.md b/cookbook/examples/streamlit_apps/gtm_outreach/README.md new file mode 100644 index 0000000000..a31a1a1c72 --- /dev/null +++ b/cookbook/examples/streamlit_apps/gtm_outreach/README.md @@ -0,0 +1,214 @@ +# GTM B2B Outreach Multi-Agent System + +A powerful multi-agent system that automates B2B outreach by finding target companies, identifying decision-makers, researching insights, and generating personalized emails. Built with AI agents using OpenAI GPT models and Exa search capabilities. + +--- + +## 🚀 Features + +- **Company Discovery**: AI-powered search to find companies matching your targeting criteria +- **Contact Identification**: Automatically discovers decision-makers and key contacts +- **Phone Number Research**: Finds phone numbers for identified contacts +- **Company Research**: Gathers insights from websites and Reddit discussions +- **Email Generation**: Creates personalized outreach emails in multiple styles +- **Interactive Dashboard**: Streamlit-based web interface for easy operation + +--- + +## 🏗️ Architecture + +The system consists of 5 specialized AI agents working in sequence: + +- **CompanyFinderAgent**: Discovers target companies using Exa search +- **ContactFinderAgent**: Identifies decision-makers and contacts +- **PhoneFinderAgent**: Researches phone numbers for contacts +- **ResearchAgent**: Collects company insights and intelligence +- **EmailWriterAgent**: Generates personalized outreach emails + +--- + +## 📋 Prerequisites + +- Python 3.8+ +- OpenAI API key +- Exa API key + +--- + +## 🛠️ Installation + +1. Clone the repository + ```bash + git clone + cd agno/cookbook/examples/streamlit_apps/gtm_outreach + ``` + +2. Create a virtual environment + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +3. Install dependencies + ```bash + # Option 1: Install from requirements.txt + pip install -r requirements.txt + + # Option 2: Generate fresh requirements (requires pip-tools) + chmod +x generate_requirements.sh + ./generate_requirements.sh + pip install -r requirements.txt + ``` + +--- + +## 🔑 API Keys Setup + +### OpenAI API Key +1. Visit [OpenAI Platform](https://platform.openai.com/) +2. Create an account or sign in +3. Go to API Keys section +4. Create a new API key + +### Exa API Key +1. Visit [Exa](https://exa.ai/) +2. Sign up for an account +3. Get your API key from the dashboard + +### Environment Variables (Optional) +```bash +export OPENAI_API_KEY="your_openai_key_here" +export EXA_API_KEY="your_exa_key_here" +``` + +--- + +## 🚀 Usage + +### Starting the Application +```bash +streamlit run app.py +``` +The application will open in your browser at [http://localhost:8501](http://localhost:8501) + +### Using the Interface +1. **Configure API Keys**: Enter your OpenAI and Exa API keys in the sidebar +2. **Define Target Companies**: Describe your ideal customer profile +3. **Describe Your Offering**: Explain what you're selling/offering +4. **Set Parameters**: + - Your name and company + - Calendar link (optional) + - Number of companies (1-10) + - Email style (Professional, Casual, Cold, Consultative) +5. Click **"Start Outreach"** to begin the automated process + +### Email Styles +- **Professional**: Clear, respectful, and businesslike tone +- **Casual**: Friendly, approachable, first-name basis +- **Cold**: Strong hook with tight value proposition +- **Consultative**: Insight-led approach with soft call-to-action + +--- + +## 📊 Output + +The system provides comprehensive results: + +- **Companies**: List of target companies with fit reasoning +- **Contacts**: Decision-makers with titles and email addresses +- **Phone Numbers**: Contact phone numbers with verification status +- **Research Insights**: Key intelligence about each company +- **Personalized Emails**: Ready-to-send outreach emails + +--- + +## 🏃‍♂️ Example Workflow + +```python +# Example target description +target_desc = """ +SaaS companies with 50-200 employees in the fintech space, +particularly those focused on payment processing or digital banking +""" + +# Example offering +offering_desc = """ +AI-powered fraud detection solution that reduces false positives +by 40% while maintaining 99.9% accuracy in fraud detection +""" +``` + +--- + +## 📁 Project Structure + +``` +gtm-b2b-outreach/ +├── app.py # Streamlit web application +├── agent.py # Agent definitions and pipeline functions +├── requirements.in # Input requirements +├── requirements.txt # Pinned dependencies +├── generate_requirements.sh # Requirements generation script +└── README.md # This file +``` + +--- + +## 🔧 Core Dependencies + +- [agno](https://github.com/agency-swarm/agno): Multi-agent framework +- [streamlit](https://streamlit.io/): Web interface +- [openai](https://openai.com/): GPT model integration +- [exa_py](https://exa.ai/): Web search capabilities +- [pydantic](https://pydantic.dev/): Data validation + +--- + +## 🛠️ Customization + +### Adding New Email Styles +```python +def get_email_style_instruction(style_key: str) -> str: + styles = { + "Professional": "Style: Professional. Clear, respectful, and businesslike.", + "YourStyle": "Your custom style instruction here.", + } + return styles.get(style_key, styles["Professional"]) +``` + +### Modifying Agent Instructions +Each agent can be customized by updating the `instructions` parameter in the respective `create_*_agent()` functions. + +### Adjusting Model Selection +```python +model=OpenAIChat(id="gpt-4") # or "gpt-3.5-turbo" +``` + +--- + +## ⚠️ Important Notes + +- **Rate Limits**: Be mindful of API rate limits for both OpenAI and Exa +- **Costs**: Monitor usage as API calls incur costs +- **Data Privacy**: Ensure compliance with data protection regulations +- **Email Deliverability**: Generated emails should be reviewed before sending + +--- + +## 🐛 Troubleshooting + +### Common Issues + +- **API Key Errors**: Verify keys are correct and have sufficient credits. Check environment variable names. +- **JSON Parsing Errors**: The system includes fallback JSON extraction. Check agent instructions for proper JSON formatting. +- **No Results Found**: Refine your target company description. Try broader search criteria. +- **Streamlit Issues**: Clear browser cache. Restart the application. Check console for errors. + +--- + +## 📈 Performance Tips + +- Start with fewer companies (3-5) for testing +- Use specific targeting criteria for better results +- Review and refine generated content before use +- Monitor API usage and costs \ No newline at end of file diff --git a/cookbook/examples/streamlit_apps/gtm_outreach/agent.py b/cookbook/examples/streamlit_apps/gtm_outreach/agent.py new file mode 100644 index 0000000000..5e3f2becf4 --- /dev/null +++ b/cookbook/examples/streamlit_apps/gtm_outreach/agent.py @@ -0,0 +1,174 @@ +import json +from typing import Any, Dict, List, Optional + +from agno.agent import Agent +from agno.memory.v2 import Memory +from agno.models.openai import OpenAIChat +from agno.tools.exa import ExaTools + + +def create_company_finder_agent() -> Agent: + exa_tools = ExaTools(category="company") + memory = Memory() + return Agent( + model=OpenAIChat(id="gpt-5"), + tools=[exa_tools], + memory=memory, + add_history_to_messages=True, + num_history_responses=6, + session_id="gtm_outreach_company_finder", + show_tool_calls=True, + instructions=[ + "You are CompanyFinderAgent. Use ExaTools to search the web for companies that match the targeting criteria.", + "Return ONLY valid JSON with key 'companies' as a list; respect the requested limit provided in the user prompt.", + "Each item must have: name, website, why_fit (1-2 lines).", + ], + ) + + +def create_contact_finder_agent() -> Agent: + exa_tools = ExaTools() + memory = Memory() + return Agent( + model=OpenAIChat(id="gpt-4o"), + tools=[exa_tools], + memory=memory, + add_history_to_messages=True, + num_history_responses=6, + session_id="gtm_outreach_contact_finder", + show_tool_calls=True, + instructions=[ + "You are ContactFinderAgent. Use ExaTools to collect as many relevant decision makers as possible per company...", + "Return ONLY valid JSON with key 'companies' as a list; each has: name, contacts: [{full_name, title, email, inferred}]", + ], + ) + + +def create_phone_finder_agent() -> Agent: + exa_tools = ExaTools() + memory = Memory() + return Agent( + model=OpenAIChat(id="gpt-4o"), + tools=[exa_tools], + memory=memory, + add_history_to_messages=True, + num_history_responses=6, + session_id="gtm_outreach_phone_finder", + show_tool_calls=True, + instructions=[ + "You are PhoneFinderAgent. Use ExaTools to find phone numbers...", + "Return ONLY valid JSON with key 'companies' as a list; each has: name, contacts: [{full_name, phone_number, phone_type, verified}]", + ], + ) + + +def create_research_agent() -> Agent: + exa_tools = ExaTools() + memory = Memory() + return Agent( + model=OpenAIChat(id="gpt-5"), + tools=[exa_tools], + memory=memory, + add_history_to_messages=True, + num_history_responses=6, + session_id="gtm_outreach_researcher", + show_tool_calls=True, + instructions=[ + "You are ResearchAgent. Collect insights from websites + Reddit...", + "Return ONLY valid JSON with key 'companies' as a list; each has: name, insights: [strings].", + ], + ) + + +def get_email_style_instruction(style_key: str) -> str: + styles = { + "Professional": "Style: Professional. Clear, respectful, and businesslike.", + "Casual": "Style: Casual. Friendly, approachable, first-name basis.", + "Cold": "Style: Cold email. Strong hook, tight value proposition.", + "Consultative": "Style: Consultative. Insight-led, soft CTA.", + } + return styles.get(style_key, styles["Professional"]) + + +def create_email_writer_agent(style_key: str = "Professional") -> Agent: + memory = Memory() + style_instruction = get_email_style_instruction(style_key) + return Agent( + model=OpenAIChat(id="gpt-5"), + tools=[], + memory=memory, + add_history_to_messages=True, + num_history_responses=6, + session_id="gtm_outreach_email_writer", + show_tool_calls=False, + instructions=[ + "You are EmailWriterAgent. Write concise, personalized B2B outreach emails.", + style_instruction, + "Return ONLY valid JSON with key 'emails' as a list of items: {company, contact, subject, body}.", + ], + ) + + +# -------- Utility Functions -------- # + + +def extract_json_or_raise(text: str) -> Dict[str, Any]: + try: + return json.loads(text) + except Exception: + start = text.find("{") + end = text.rfind("}") + if start != -1 and end != -1 and end > start: + return json.loads(text[start : end + 1]) + raise + + +def run_company_finder( + agent: Agent, target_desc: str, offering_desc: str, max_companies: int +) -> List[Dict[str, str]]: + prompt = f"Find exactly {max_companies} companies...\nTargeting: {target_desc}\nOffering: {offering_desc}" + resp = agent.run(prompt) + data = extract_json_or_raise(str(resp.content)) + return data.get("companies", [])[: max(1, min(max_companies, 10))] + + +def run_contact_finder( + agent: Agent, companies: List[Dict[str, str]], target_desc: str, offering_desc: str +) -> List[Dict[str, Any]]: + prompt = f"For each company below, find contacts...\nCompanies JSON: {json.dumps(companies)}" + resp = agent.run(prompt) + data = extract_json_or_raise(str(resp.content)) + return data.get("companies", []) + + +def run_phone_finder( + agent: Agent, contacts_data: List[Dict[str, Any]] +) -> List[Dict[str, Any]]: + prompt = f"For each contact below, find phone numbers...\nContacts JSON: {json.dumps(contacts_data)}" + resp = agent.run(prompt) + data = extract_json_or_raise(str(resp.content)) + return data.get("companies", []) + + +def run_research(agent: Agent, companies: List[Dict[str, str]]) -> List[Dict[str, Any]]: + prompt = ( + f"For each company, gather insights...\nCompanies JSON: {json.dumps(companies)}" + ) + resp = agent.run(prompt) + data = extract_json_or_raise(str(resp.content)) + return data.get("companies", []) + + +def run_email_writer( + agent: Agent, + contacts_data: List[Dict[str, Any]], + research_data: List[Dict[str, Any]], + offering_desc: str, + sender_name: str, + sender_company: str, + calendar_link: Optional[str], +) -> List[Dict[str, str]]: + prompt = f"Write outreach emails...\nContacts JSON: {json.dumps(contacts_data)}\nResearch JSON: {json.dumps(research_data)}" + resp = agent.run(prompt) + data = extract_json_or_raise(str(resp.content)) + return data.get("emails", []) diff --git a/cookbook/examples/streamlit_apps/gtm_outreach/app.py b/cookbook/examples/streamlit_apps/gtm_outreach/app.py new file mode 100644 index 0000000000..6ba2727114 --- /dev/null +++ b/cookbook/examples/streamlit_apps/gtm_outreach/app.py @@ -0,0 +1,158 @@ +import os + +import streamlit as st +from agent import ( + create_company_finder_agent, + create_contact_finder_agent, + create_email_writer_agent, + create_phone_finder_agent, + create_research_agent, + run_company_finder, + run_contact_finder, + run_email_writer, + run_phone_finder, + run_research, +) + + +def main() -> None: + st.set_page_config(page_title="GTM B2B Outreach", layout="wide") + + # Sidebar: API keys + st.sidebar.header("API Configuration") + openai_key = st.sidebar.text_input( + "OpenAI API Key", type="password", value=os.getenv("OPENAI_API_KEY", "") + ) + exa_key = st.sidebar.text_input( + "Exa API Key", type="password", value=os.getenv("EXA_API_KEY", "") + ) + if openai_key: + os.environ["OPENAI_API_KEY"] = openai_key + if exa_key: + os.environ["EXA_API_KEY"] = exa_key + + if not openai_key or not exa_key: + st.sidebar.warning("Enter both API keys to enable the app") + + # Inputs + st.title("GTM B2B Outreach Multi Agent Team") + col1, col2 = st.columns(2) + with col1: + target_desc = st.text_area("Target companies", height=100) + offering_desc = st.text_area("Your offering", height=100) + with col2: + sender_name = st.text_input("Your name", value="Sales Team") + sender_company = st.text_input("Your company", value="Our Company") + calendar_link = st.text_input("Calendar link (optional)", value="") + num_companies = st.number_input( + "Number of companies", min_value=1, max_value=10, value=5 + ) + email_style = st.selectbox( + "Email style", ["Professional", "Casual", "Cold", "Consultative"] + ) + + if st.button("Start Outreach", type="primary"): + if not openai_key or not exa_key: + st.error("Please provide API keys") + elif not target_desc or not offering_desc: + st.error("Please fill in target companies and offering") + else: + progress = st.progress(0) + stage_msg = st.empty() + details = st.empty() + try: + company_agent = create_company_finder_agent() + contact_agent = create_contact_finder_agent() + phone_agent = create_phone_finder_agent() + research_agent = create_research_agent() + email_agent = create_email_writer_agent(email_style) + + # Run pipeline + stage_msg.info("1/5 Finding companies...") + companies = run_company_finder( + company_agent, + target_desc.strip(), + offering_desc.strip(), + int(num_companies), + ) + progress.progress(20) + + stage_msg.info("2/5 Finding contacts...") + contacts_data = run_contact_finder( + contact_agent, companies, target_desc, offering_desc + ) + progress.progress(40) + + stage_msg.info("3/5 Finding phones...") + phone_data = run_phone_finder(phone_agent, contacts_data) + progress.progress(60) + + stage_msg.info("4/5 Researching insights...") + research_data = run_research(research_agent, companies) + progress.progress(80) + + stage_msg.info("5/5 Writing emails...") + emails = run_email_writer( + email_agent, + contacts_data, + research_data, + offering_desc, + sender_name, + sender_company, + calendar_link, + ) + progress.progress(100) + + st.session_state["gtm_results"] = { + "companies": companies, + "contacts": contacts_data, + "phones": phone_data, + "research": research_data, + "emails": emails, + } + stage_msg.success("Completed") + except Exception as e: + stage_msg.error("Pipeline failed") + st.error(str(e)) + + # Results + results = st.session_state.get("gtm_results") + if results: + st.subheader("Top Companies") + for idx, c in enumerate(results["companies"], 1): + st.markdown(f"**{idx}. {c.get('name', '')}**") + st.write(c.get("website", "")) + st.write(c.get("why_fit", "")) + + st.subheader("Contacts") + for c in results["contacts"]: + st.markdown(f"**{c.get('name', '')}**") + for p in c.get("contacts", []): + inferred = " (inferred)" if p.get("inferred") else "" + st.write( + f"- {p.get('full_name', '')} | {p.get('title', '')} | {p.get('email', '')}{inferred}" + ) + + st.subheader("Phones") + for c in results["phones"]: + st.markdown(f"**{c.get('name', '')}**") + for p in c.get("contacts", []): + st.write( + f"- {p.get('full_name', '')} | {p.get('phone_number', '')} ({p.get('phone_type', '')}) {'✓' if p.get('verified') else '~'}" + ) + + st.subheader("Research Insights") + for r in results["research"]: + st.markdown(f"**{r.get('name', '')}**") + for ins in r.get("insights", []): + st.write(f"- {ins}") + + st.subheader("Emails") + for i, e in enumerate(results["emails"], 1): + with st.expander(f"{i}. {e.get('company', '')} → {e.get('contact', '')}"): + st.write(f"Subject: {e.get('subject', '')}") + st.text(e.get("body", "")) + + +if __name__ == "__main__": + main() diff --git a/cookbook/examples/streamlit_apps/gtm_outreach/generate_requirements.sh b/cookbook/examples/streamlit_apps/gtm_outreach/generate_requirements.sh new file mode 100644 index 0000000000..efdfeb9fed --- /dev/null +++ b/cookbook/examples/streamlit_apps/gtm_outreach/generate_requirements.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Generate pinned requirements.txt from requirements.in +# Requires pip-tools (pip-compile) + +set -e + +if ! command -v pip-compile &> /dev/null; then + echo "⚠️ pip-tools not found. Installing..." + pip install pip-tools +fi + +echo "📦 Compiling requirements..." +pip-compile requirements.in --output-file requirements.txt + +echo "✅ requirements.txt generated from requirements.in" diff --git a/cookbook/examples/streamlit_apps/gtm_outreach/requirements.in b/cookbook/examples/streamlit_apps/gtm_outreach/requirements.in new file mode 100644 index 0000000000..08fd7e21ca --- /dev/null +++ b/cookbook/examples/streamlit_apps/gtm_outreach/requirements.in @@ -0,0 +1,5 @@ +agno>=0.4.2 +streamlit>=1.33.0 +pydantic>=2.7.0 +openai>=1.30.0 +exa_py>=1.0.7 \ No newline at end of file diff --git a/cookbook/examples/streamlit_apps/gtm_outreach/requirements.txt b/cookbook/examples/streamlit_apps/gtm_outreach/requirements.txt new file mode 100644 index 0000000000..9cfc684a7f --- /dev/null +++ b/cookbook/examples/streamlit_apps/gtm_outreach/requirements.txt @@ -0,0 +1,191 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --output-file=requirements.txt requirements.in +# +agno==1.7.12 + # via -r requirements.in +altair==5.5.0 + # via streamlit +annotated-types==0.7.0 + # via pydantic +anyio==4.10.0 + # via + # httpx + # openai +attrs==25.3.0 + # via + # jsonschema + # referencing +blinker==1.9.0 + # via streamlit +cachetools==6.1.0 + # via streamlit +certifi==2025.8.3 + # via + # httpcore + # httpx + # requests +charset-normalizer==3.4.3 + # via requests +click==8.2.1 + # via + # streamlit + # typer +distro==1.9.0 + # via openai +docstring-parser==0.17.0 + # via agno +exa-py==1.15.1 + # via -r requirements.in +gitdb==4.0.12 + # via gitpython +gitpython==3.1.45 + # via + # agno + # streamlit +h11==0.16.0 + # via httpcore +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # agno + # exa-py + # openai +idna==3.10 + # via + # anyio + # httpx + # requests +jinja2==3.1.6 + # via + # altair + # pydeck +jiter==0.10.0 + # via openai +jsonschema==4.25.1 + # via altair +jsonschema-specifications==2025.4.1 + # via jsonschema +markdown-it-py==4.0.0 + # via rich +markupsafe==3.0.2 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +narwhals==2.1.2 + # via altair +numpy==2.3.2 + # via + # pandas + # pydeck + # streamlit +openai==1.101.0 + # via + # -r requirements.in + # exa-py +packaging==25.0 + # via + # agno + # altair + # streamlit +pandas==2.3.2 + # via streamlit +pillow==11.3.0 + # via streamlit +protobuf==6.32.0 + # via streamlit +pyarrow==21.0.0 + # via streamlit +pydantic==2.11.7 + # via + # -r requirements.in + # agno + # exa-py + # openai + # pydantic-settings +pydantic-core==2.33.2 + # via pydantic +pydantic-settings==2.10.1 + # via agno +pydeck==0.9.1 + # via streamlit +pygments==2.19.2 + # via rich +python-dateutil==2.9.0.post0 + # via pandas +python-dotenv==1.1.1 + # via + # agno + # pydantic-settings +python-multipart==0.0.20 + # via agno +pytz==2025.2 + # via pandas +pyyaml==6.0.2 + # via agno +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications +requests==2.32.5 + # via + # exa-py + # streamlit +rich==14.1.0 + # via + # agno + # typer +rpds-py==0.27.0 + # via + # jsonschema + # referencing +shellingham==1.5.4 + # via typer +six==1.17.0 + # via python-dateutil +smmap==5.0.2 + # via gitdb +sniffio==1.3.1 + # via + # anyio + # openai +streamlit==1.48.1 + # via -r requirements.in +tenacity==9.1.2 + # via streamlit +toml==0.10.2 + # via streamlit +tomli==2.2.1 + # via agno +tornado==6.5.2 + # via streamlit +tqdm==4.67.1 + # via openai +typer==0.16.1 + # via agno +typing-extensions==4.14.1 + # via + # agno + # altair + # anyio + # exa-py + # openai + # pydantic + # pydantic-core + # referencing + # streamlit + # typer + # typing-inspection +typing-inspection==0.4.1 + # via + # pydantic + # pydantic-settings +tzdata==2025.2 + # via pandas +urllib3==2.5.0 + # via requests +watchdog==6.0.0 + # via streamlit