Spaces:
Runtime error
Runtime error
| #!/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") | |
| 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 | |
| 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...") | |