ai-lead-generation / get_tools_working.py
togitoon's picture
Fix version conflict
3322d67
raw
history blame
16.9 kB
#!/usr/bin/env python3
"""
LinkedIn Lead Generation Agent with Outreach Message Creation
An AI-powered agent that generates high-quality recruitment leads by:
1. Searching for target companies based on funding news and hiring signals
2. Finding LinkedIn profiles of key decision makers and hiring personnel
3. Extracting contact information and professional details
4. Crafting personalized outreach message templates for each contact
This agent implements a comprehensive lead generation workflow:
1. Extract: Search for companies and funding announcements using web search
2. Transform: Find LinkedIn profiles of decision makers and analyze their roles
3. Load: Generate comprehensive reports with LinkedIn URLs and personalized outreach messages
"""
import os
import json
import pandas as pd
from datetime import datetime
import requests
import sys
import xml.etree.ElementTree as ET
from botocore.config import Config as BotocoreConfig
from datetime import date
from strands import Agent, tool
from strands.models import BedrockModel
from strands_tools import workflow
from strands_tools import rss
from strands_tools import http_request
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
# Allow full tool use without prompting
os.environ.setdefault("BYPASS_TOOL_CONSENT", "true")
@tool
def brave_search(query: str, count: int = 10) -> str:
"""Search the web using Brave Search API for news and information via direct HTTP requests"""
print(f"πŸ” Searching Brave for: '{query}' (count: {count})")
# Get Brave API key from environment variable
brave_api_key = os.getenv("BRAVE_API_KEY")
if not brave_api_key:
raise ValueError("BRAVE_API_KEY environment variable is not set")
try:
# Brave Search API endpoint
api_url = "https://api.search.brave.com/res/v1/web/search"
# Set up headers with API key
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": brave_api_key
}
# Set up query parameters
params = {
"q": query,
"count": min(count, 20), # API maximum is 20
"offset": 0,
"safesearch": "moderate",
"freshness": "",
"text_decorations": True,
"spellcheck": True,
"result_filter": "web,news" # Include both web and news results
}
# Make the HTTP request
print(f"πŸ“‘ Making request to Brave Search API...")
response = requests.get(api_url, headers=headers, params=params, timeout=30)
# Check if request was successful
if response.status_code != 200:
error_msg = f"Brave Search API returned status {response.status_code}: {response.text}"
print(f"❌ {error_msg}")
return error_msg
# Parse JSON response
search_data = response.json()
formatted_results = []
# Process web results
if "web" in search_data and "results" in search_data["web"]:
web_results = search_data["web"]["results"]
for idx, result in enumerate(web_results[:count], 1):
formatted_result = f"""
Result {idx}:
Title: {result.get('title', 'N/A')}
URL: {result.get('url', 'N/A')}
Description: {result.get('description', 'N/A')}
---"""
formatted_results.append(formatted_result)
# Process news results if available
if "news" in search_data and "results" in search_data["news"]:
news_results = search_data["news"]["results"]
if news_results:
formatted_results.append("\nπŸ—žοΈ NEWS RESULTS:")
for idx, news in enumerate(news_results[:count], 1):
formatted_result = f"""
News {idx}:
Title: {news.get('title', 'N/A')}
URL: {news.get('url', 'N/A')}
Description: {news.get('description', 'N/A')}
Age: {news.get('age', 'N/A')}
---"""
formatted_results.append(formatted_result)
final_result = "\n".join(formatted_results) if formatted_results else "No results found"
print(f"βœ… Found {len(formatted_results)} results from Brave Search API")
return final_result
except requests.exceptions.Timeout:
error_msg = "Request to Brave Search API timed out"
print(f"❌ {error_msg}")
return error_msg
except requests.exceptions.RequestException as e:
error_msg = f"Network error with Brave Search API: {str(e)}"
print(f"❌ {error_msg}")
return error_msg
except json.JSONDecodeError as e:
error_msg = f"Failed to parse Brave Search API response: {str(e)}"
print(f"❌ {error_msg}")
return error_msg
except Exception as e:
error_msg = f"Error performing Brave search: {str(e)}"
print(f"❌ {error_msg}")
return error_msg
@tool
def send_email(to: str, html_content: str, subject: str) -> str:
"""Send an email to given address"""
print(f"Sending to {to} content {html_content} and subject {subject}.")
# Get SendGrid API key from environment variable
sendgrid_api_key = os.getenv("SENDGRID_API_KEY")
if not sendgrid_api_key:
raise ValueError("SENDGRID_API_KEY environment variable is not set")
message = Mail(
from_email='kavukcu.tolga@gmail.com',
to_emails=to,
subject=subject,
html_content=html_content
)
try:
sg = SendGridAPIClient(sendgrid_api_key)
# sg.set_sendgrid_data_residency("eu")
# uncomment the above line if you are sending mail using a regional EU subuser
response = sg.send(message)
print(response.status_code)
print(response.body)
print(response.headers)
return f"Mail is sent to {to}"
except Exception as e:
print(f"Error sending email: {str(e)}")
return f"Failed to send email to {to}: {str(e)}"
def get_leadgen_agent(user_email: str):
"""
Returns a configured agent for lead generation.
Args:
user_email: The email address to send the report to (required - no default)
"""
print(f"\nπŸ”§ CREATING LEADGEN AGENT FOR: {user_email}")
print("-" * 50)
if not user_email:
print("❌ ERROR: user_email is required and cannot be empty")
raise ValueError("user_email is required and cannot be empty")
print("βœ… Email validation passed")
print("πŸ€– Creating Bedrock model configuration...")
custom_config = BotocoreConfig(read_timeout=3600)
print(f" - Read timeout: 3600 seconds")
print(f" - Model ID: us.anthropic.claude-3-7-sonnet-20250219-v1:0")
print(f" - Region: us-west-2")
print(f" - Temperature: 0.2")
print(f" - Max tokens: 8000")
bedrock_model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
temperature=0.2,
region_name="us-west-2",
boto_client_config=custom_config,
streaming=True,
max_tokens=32000, # Set explicit token limit
)
print("βœ… Bedrock model created successfully")
# Generic system instructions for Docker environment with LinkedIn profile search and outreach
autonomous_system_prompt = f"""
CRITICAL: You are running in a Docker container environment and MUST operate completely autonomously without ANY user input or interaction.
ENVIRONMENT CONTEXT:
- You are running in a headless Docker container
- No user interface is available
- You cannot ask questions or wait for user responses
- You must make all decisions automatically and proceed independently
- Never prompt for confirmations or additional input
AVAILABLE TOOLS AND BEST PRACTICES:
1. BRAVE SEARCH TOOL (brave_search) - Use for web searches, news discovery, and LinkedIn profile searches
- Best for: Real-time web searches, finding news articles, funding announcements, LinkedIn profiles
- LinkedIn Profile Search Examples:
```
# Search for company decision makers
brave_search("site:linkedin.com/in CEO founder \"Company Name\"", 10)
# Search for specific roles at companies
brave_search("site:linkedin.com/in \"Head of Talent\" \"VP Recruiting\" \"Company Name\"", 8)
# Search for hiring managers in specific industries
brave_search("site:linkedin.com/in \"Talent Acquisition\" fintech startup", 10)
# Search for contact persons by title and company
brave_search("site:linkedin.com/in \"Chief People Officer\" \"HR Director\" \"Company Name\"", 5)
```
- General Search Examples:
```
# Search for funding news
brave_search("startup funding series A Europe 2024", 10)
# Search for specific company news
brave_search("TechCorp AI funding announcement hiring plans", 5)
# Search for industry trends
brave_search("fintech hiring trends Middle East recruitment", 8)
```
2. HTTP REQUEST TOOL (http_request) - Use for fetching web pages and APIs
- Best for: Fetching specific web pages, API calls, converting HTML to markdown
- Examples:
```
# Fetch a web page
http_request({{
"method": "GET",
"url": "https://example.com/page",
"convert_to_markdown": True
}})
# API call with authentication
http_request({{
"method": "POST",
"url": "https://api.example.com/endpoint",
"headers": {{"Content-Type": "application/json"}},
"body": json.dumps({{"key": "value"}}),
"auth_type": "Bearer",
"auth_token": "your_token"
}})
```
3. RSS TOOL (rss) - Use for news feeds and RSS content management
- Best for: Subscribing to feeds, reading news content, searching feed content
- Examples:
```
# Fetch without subscribing
rss({{
"action": "fetch",
"url": "https://example.com/feed",
"max_entries": 3
}})
```
4. EMAIL TOOL (send_email) - Use for sending reports and notifications
- Best for: Delivering final results, reports, notifications
- Automatically sends HTML formatted emails
LINKEDIN PROFILE SEARCH STRATEGY:
1. **Identify Target Companies**: From funding announcements, news, or specified criteria
2. **Search for Decision Makers**: Use site:linkedin.com/in searches to find key personnel
3. **Target Key Roles**: Focus on hiring managers, HR directors, CTOs, founders, talent acquisition specialists
4. **Verify Profiles**: Extract LinkedIn URLs and validate they are actual decision makers
5. **Gather Profile Information**: Name, title, company, industry experience
SEARCH QUERIES FOR LINKEDIN PROFILES:
- "site:linkedin.com/in CEO founder [Company Name]"
- "site:linkedin.com/in \"Head of Talent\" \"VP Recruiting\" [Company Name]"
- "site:linkedin.com/in \"Chief Technology Officer\" \"Engineering Manager\" [Company Name]"
- "site:linkedin.com/in \"Chief People Officer\" \"HR Director\" [Company Name]"
- "site:linkedin.com/in \"Talent Acquisition\" [Industry] [Location]"
OUTREACH MESSAGE CRAFTING:
For each LinkedIn profile found, create personalized outreach messages that include:
1. **Subject Line Templates**:
- "Partnership opportunity for [Company Name]'s talent acquisition"
- "Helping [Company Name] scale your [Department] team"
- "Talent solutions for [Company Name]'s growth phase"
2. **Message Structure**:
- Personal greeting using their name and title
- Reference their company's recent news/funding/growth
- Specific value proposition for their recruitment needs
- Clear call-to-action
- Professional closing
3. **Personalization Elements**:
- Recent company milestones (funding, product launches, expansions)
- Industry-specific challenges they likely face
- Role-specific pain points (e.g., technical hiring for CTOs)
- Geographic relevance if applicable
4. **Message Examples**:
```
Subject: Talent solutions for [Company]'s Series A growth phase
Hi [Name],
Congratulations on [Company]'s recent $X Series A funding! As you scale your [department/team],
I imagine finding top-tier [specific roles] talent is becoming increasingly critical.
We've helped similar [industry] companies in [location] build exceptional teams during
rapid growth phases. Our specialized approach to [relevant skill area] recruitment
could be valuable as [Company] enters this exciting expansion phase.
Would you be open to a brief 15-minute conversation about your current talent priorities?
Best regards,
[Your name]
```
TOOL SELECTION STRATEGY:
1. **Start with Brave Search** for company news, funding announcements, and LinkedIn profile discovery
2. **Use LinkedIn-specific searches** to find decision makers and contact persons
3. **Use HTTP request** for direct web page fetching when needed
4. **Use RSS tool** for ongoing news monitoring
5. **Craft personalized outreach messages** for each profile found
6. **Use email tool** to deliver final report with leads and outreach templates
AUTONOMOUS OPERATION REQUIREMENTS:
1. Search for and identify target companies from news/funding announcements
2. Find LinkedIn profiles of key decision makers at these companies
3. Extract contact information and professional details
4. Create personalized outreach messages for each contact
5. Generate comprehensive lead reports with LinkedIn URLs and message templates
6. Handle errors gracefully and continue processing
7. Complete the entire workflow autonomously
LEAD GENERATION WORKFLOW:
1. **Discover Companies**: Search for funding news, hiring announcements, expansion news
2. **Find LinkedIn Profiles**: Search for decision makers using site:linkedin.com/in queries
3. **Validate Contacts**: Ensure profiles are relevant and current
4. **Extract Information**: Company, name, title, LinkedIn URL, recent company news
5. **Craft Messages**: Create personalized outreach templates
6. **Compile Report**: Generate comprehensive lead list with contact details and messages
7. **Send Email Report**: Deliver results with LinkedIn URLs and outreach templates
IMPORTANT: For each lead found, you MUST:
- Include the LinkedIn profile URL
- Provide the person's name, title, and company
- Create a personalized outreach message template
- Reference recent company news or developments
- Always send a final email report with all leads, LinkedIn URLs, and outreach messages
IMPORTANT : When you need to use a date , month or year be aware today is {str(date.today())}
Remember: You are in a server environment - operate completely independently without any user interaction. Focus on finding LinkedIn profiles and crafting personalized outreach messages for recruitment leads.
"""
print("🀝 Creating Agent with tools and model...")
print(f" - Tools: send_email, rss, http_request, brave_search")
print(f" - Model: {bedrock_model}")
print(f" - System prompt length: {len(autonomous_system_prompt)} characters")
# Agent with autonomous system prompt for Docker environment including standard strands tools
try:
agent = Agent(
tools=[send_email, rss, http_request, brave_search],
model=bedrock_model,
system_prompt=autonomous_system_prompt
)
print("βœ… Agent created successfully")
# Test agent method availability
print("πŸ§ͺ Testing agent methods...")
print(f" - Agent has __call__ method: {hasattr(agent, '__call__')}")
print(f" - Agent tools: {[tool.__name__ if hasattr(tool, '__name__') else str(tool) for tool in agent.tools] if hasattr(agent, 'tools') else 'No tools attribute'}")
print(f" - Agent model: {type(agent.model).__name__ if hasattr(agent, 'model') else 'No model attribute'}")
except Exception as e:
print(f"❌ ERROR creating agent: {e}")
import traceback
traceback.print_exc()
raise
print(f"🎯 Agent setup completed for email: {user_email}")
print("-" * 50)
return agent
if __name__ == "__main__":
print("Initializing recruitment lead generation agent...")
# Print Python version
print(f"Python version: {sys.version}")
# Create the Bedrock model with Claude 4 Sonnet
print("Creating Bedrock model...")
# The model and prompt are now handled by get_leadgen_agent_and_prompt()
# custom_config = BotocoreConfig(read_timeout=3600)
# bedrock_model = BedrockModel(
# model_id="us.amazon.nova-pro-v1:0",
# temperature=0.2,
# region_name="us-west-2", # Explicitly set the region to us-west-2
# boto_client_config=custom_config,
# streaming=False,
# )
# print(f"Bedrock model type: {type(bedrock_model)}")
# Create the main agent
print("Creating agent...")