mirror of https://github.com/kortix-ai/suna.git
Merge pull request #38 from kortix-ai/rapid-api-base
Rapid api base, with LinkedIN
This commit is contained in:
commit
a81c7db5a5
19
README.md
19
README.md
|
@ -13,9 +13,9 @@ Suna's powerful toolkit includes seamless browser automation to navigate the web
|
|||
[](./license)
|
||||
[](https://discord.gg/Py6pCBUUPw)
|
||||
[](https://x.com/kortixai)
|
||||
[](https://github.com/kortix-ai/agentpress)
|
||||
[](https://github.com/kortix-ai/agentpress/labels/bug)
|
||||
[](https://github.com/kortix-ai/suna)
|
||||
[](https://github.com/kortix-ai/suna/labels/bug)
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -136,12 +136,20 @@ You'll need the following components:
|
|||
5. **Search API Key** (Optional):
|
||||
- For enhanced search capabilities, obtain an [Exa API key](https://dashboard.exa.ai/playground)
|
||||
|
||||
6. **RapidAPI API Key** (Optional):
|
||||
- To enable API services like LinkedIn, and others, you'll need a RapidAPI key
|
||||
- Each service requires individual activation in your RapidAPI account:
|
||||
1. Locate the service's `base_url` in its corresponding file (e.g., `"https://linkedin-data-scraper.p.rapidapi.com"` in [`backend/agent/tools/api_services/LinkedInService.py`](backend/agent/tools/api_services/LinkedInService.py))
|
||||
2. Visit that specific API on the RapidAPI marketplace
|
||||
3. Subscribe to the service (many offer free tiers with limited requests)
|
||||
4. Once subscribed, the service will be available to your agent through the API Services tool
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone https://github.com/kortix-ai/agentpress.git
|
||||
cd agentpress
|
||||
git clone https://github.com/kortix-ai/suna.git
|
||||
cd suna
|
||||
```
|
||||
|
||||
2. **Configure backend environment**:
|
||||
|
@ -181,6 +189,7 @@ MODEL_TO_USE="gpt-4o"
|
|||
|
||||
# Optional but recommended
|
||||
EXA_API_KEY=your_exa_api_key # Optional
|
||||
RAPID_API_KEY=
|
||||
```
|
||||
|
||||
3. **Set up Supabase database**:
|
||||
|
|
|
@ -14,10 +14,10 @@ from agentpress.response_processor import ProcessorConfig
|
|||
from agent.tools.sb_shell_tool import SandboxShellTool
|
||||
from agent.tools.sb_files_tool import SandboxFilesTool
|
||||
from agent.tools.sb_browser_tool import SandboxBrowserTool
|
||||
from agent.tools.api_services_tool import APIServicesTool
|
||||
from agent.prompt import get_system_prompt
|
||||
from sandbox.sandbox import daytona, create_sandbox, get_or_start_sandbox
|
||||
from sandbox.sandbox import create_sandbox, get_or_start_sandbox
|
||||
from utils.billing import check_billing_status, get_account_id_from_thread
|
||||
from utils.db import update_agent_run_status
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
@ -73,6 +73,9 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread
|
|||
if os.getenv("EXA_API_KEY"):
|
||||
thread_manager.add_tool(WebSearchTool)
|
||||
|
||||
if os.getenv("RAPID_API_KEY"):
|
||||
thread_manager.add_tool(APIServicesTool)
|
||||
|
||||
xml_examples = ""
|
||||
for tag_name, example in thread_manager.tool_registry.get_xml_examples().items():
|
||||
xml_examples += f"{example}\n"
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import os
|
||||
import requests
|
||||
from typing import Dict, Any, Optional, TypedDict, Literal
|
||||
|
||||
|
||||
class EndpointSchema(TypedDict):
|
||||
route: str
|
||||
method: Literal['GET', 'POST']
|
||||
name: str
|
||||
description: str
|
||||
payload: Dict[str, Any]
|
||||
|
||||
|
||||
class APIServicesBase:
|
||||
def __init__(self, base_url: str, endpoints: Dict[str, EndpointSchema]):
|
||||
self.base_url = base_url
|
||||
self.endpoints = endpoints
|
||||
|
||||
def get_endpoints(self):
|
||||
return self.endpoints
|
||||
|
||||
def call_endpoint(
|
||||
self,
|
||||
route: str,
|
||||
payload: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""
|
||||
Call an API endpoint with the given parameters and data.
|
||||
|
||||
Args:
|
||||
endpoint (EndpointSchema): The endpoint configuration dictionary
|
||||
params (dict, optional): Query parameters for GET requests
|
||||
payload (dict, optional): JSON payload for POST requests
|
||||
|
||||
Returns:
|
||||
dict: The JSON response from the API
|
||||
"""
|
||||
if route.startswith("/"):
|
||||
route = route[1:]
|
||||
|
||||
endpoint = self.endpoints.get(route)
|
||||
if not endpoint:
|
||||
raise ValueError(f"Endpoint {route} not found")
|
||||
|
||||
url = f"{self.base_url}{endpoint['route']}"
|
||||
|
||||
headers = {
|
||||
"x-rapidapi-key": os.getenv("RAPID_API_KEY"),
|
||||
"x-rapidapi-host": url.split("//")[1].split("/")[0],
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
method = endpoint.get('method', 'GET').upper()
|
||||
|
||||
if method == 'GET':
|
||||
response = requests.get(url, params=payload, headers=headers)
|
||||
elif method == 'POST':
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
return response.json()
|
|
@ -0,0 +1,250 @@
|
|||
from typing import Dict
|
||||
|
||||
from agent.tools.api_services.APIServicesBase import APIServicesBase, EndpointSchema
|
||||
|
||||
|
||||
class LinkedInService(APIServicesBase):
|
||||
def __init__(self):
|
||||
endpoints: Dict[str, EndpointSchema] = {
|
||||
"person": {
|
||||
"route": "/person",
|
||||
"method": "POST",
|
||||
"name": "Person Data",
|
||||
"description": "Fetches any Linkedin profiles data including skills, certificates, experiences, qualifications and much more.",
|
||||
"payload": {
|
||||
"link": "LinkedIn Profile URL"
|
||||
}
|
||||
},
|
||||
"person_urn": {
|
||||
"route": "/person_urn",
|
||||
"method": "POST",
|
||||
"name": "Person Data (Using Urn)",
|
||||
"description": "It takes profile urn instead of profile public identifier in input",
|
||||
"payload": {
|
||||
"link": "LinkedIn Profile URL or URN"
|
||||
}
|
||||
},
|
||||
"person_deep": {
|
||||
"route": "/person_deep",
|
||||
"method": "POST",
|
||||
"name": "Person Data (Deep)",
|
||||
"description": "Fetches all experiences, educations, skills, languages, publications... related to a profile.",
|
||||
"payload": {
|
||||
"link": "LinkedIn Profile URL"
|
||||
}
|
||||
},
|
||||
"profile_updates": {
|
||||
"route": "/profile_updates",
|
||||
"method": "GET",
|
||||
"name": "Person Posts (WITH PAGINATION)",
|
||||
"description": "Fetches posts of a linkedin profile alongwith reactions, comments, postLink and reposts data.",
|
||||
"payload": {
|
||||
"profile_url": "LinkedIn Profile URL",
|
||||
"page": "Page number",
|
||||
"reposts": "Include reposts (1 or 0)",
|
||||
"comments": "Include comments (1 or 0)"
|
||||
}
|
||||
},
|
||||
"profile_recent_comments": {
|
||||
"route": "/profile_recent_comments",
|
||||
"method": "POST",
|
||||
"name": "Person Recent Activity (Comments on Posts)",
|
||||
"description": "Fetches 20 most recent comments posted by a linkedin user (per page).",
|
||||
"payload": {
|
||||
"profile_url": "LinkedIn Profile URL",
|
||||
"page": "Page number",
|
||||
"paginationToken": "Token for pagination"
|
||||
}
|
||||
},
|
||||
"comments_from_recent_activity": {
|
||||
"route": "/comments_from_recent_activity",
|
||||
"method": "GET",
|
||||
"name": "Comments from recent activity",
|
||||
"description": "Fetches recent comments posted by a person as per his recent activity tab.",
|
||||
"payload": {
|
||||
"profile_url": "LinkedIn Profile URL",
|
||||
"page": "Page number"
|
||||
}
|
||||
},
|
||||
"person_skills": {
|
||||
"route": "/person_skills",
|
||||
"method": "POST",
|
||||
"name": "Person Skills",
|
||||
"description": "Scraper all skills of a linkedin user",
|
||||
"payload": {
|
||||
"link": "LinkedIn Profile URL"
|
||||
}
|
||||
},
|
||||
"email_to_linkedin_profile": {
|
||||
"route": "/email_to_linkedin_profile",
|
||||
"method": "POST",
|
||||
"name": "Email to LinkedIn Profile",
|
||||
"description": "Finds LinkedIn profile associated with an email address",
|
||||
"payload": {
|
||||
"email": "Email address to search"
|
||||
}
|
||||
},
|
||||
"company": {
|
||||
"route": "/company",
|
||||
"method": "POST",
|
||||
"name": "Company Data",
|
||||
"description": "Fetches LinkedIn company profile data",
|
||||
"payload": {
|
||||
"link": "LinkedIn Company URL"
|
||||
}
|
||||
},
|
||||
"web_domain": {
|
||||
"route": "/web-domain",
|
||||
"method": "POST",
|
||||
"name": "Web Domain to Company",
|
||||
"description": "Fetches LinkedIn company profile data from a web domain",
|
||||
"payload": {
|
||||
"link": "Website domain (e.g., huzzle.app)"
|
||||
}
|
||||
},
|
||||
"similar_profiles": {
|
||||
"route": "/similar_profiles",
|
||||
"method": "GET",
|
||||
"name": "Similar Profiles",
|
||||
"description": "Fetches profiles similar to a given LinkedIn profile",
|
||||
"payload": {
|
||||
"profileUrl": "LinkedIn Profile URL"
|
||||
}
|
||||
},
|
||||
"company_jobs": {
|
||||
"route": "/company_jobs",
|
||||
"method": "POST",
|
||||
"name": "Company Jobs",
|
||||
"description": "Fetches job listings from a LinkedIn company page",
|
||||
"payload": {
|
||||
"company_url": "LinkedIn Company URL",
|
||||
"count": "Number of job listings to fetch"
|
||||
}
|
||||
},
|
||||
"company_updates": {
|
||||
"route": "/company_updates",
|
||||
"method": "GET",
|
||||
"name": "Company Posts",
|
||||
"description": "Fetches posts from a LinkedIn company page",
|
||||
"payload": {
|
||||
"company_url": "LinkedIn Company URL",
|
||||
"page": "Page number",
|
||||
"reposts": "Include reposts (0, 1, or 2)",
|
||||
"comments": "Include comments (0, 1, or 2)"
|
||||
}
|
||||
},
|
||||
"company_employee": {
|
||||
"route": "/company_employee",
|
||||
"method": "GET",
|
||||
"name": "Company Employees",
|
||||
"description": "Fetches employees of a LinkedIn company using company ID",
|
||||
"payload": {
|
||||
"companyId": "LinkedIn Company ID",
|
||||
"page": "Page number"
|
||||
}
|
||||
},
|
||||
"company_updates_post": {
|
||||
"route": "/company_updates",
|
||||
"method": "POST",
|
||||
"name": "Company Posts (POST)",
|
||||
"description": "Fetches posts from a LinkedIn company page with specific count parameters",
|
||||
"payload": {
|
||||
"company_url": "LinkedIn Company URL",
|
||||
"posts": "Number of posts to fetch",
|
||||
"comments": "Number of comments to fetch per post",
|
||||
"reposts": "Number of reposts to fetch"
|
||||
}
|
||||
},
|
||||
"search_posts_with_filters": {
|
||||
"route": "/search_posts_with_filters",
|
||||
"method": "GET",
|
||||
"name": "Search Posts With Filters",
|
||||
"description": "Searches LinkedIn posts with various filtering options",
|
||||
"payload": {
|
||||
"query": "Keywords/Search terms (text you put in LinkedIn search bar)",
|
||||
"page": "Page number (1-100, each page contains 20 results)",
|
||||
"sort_by": "Sort method: 'relevance' (Top match) or 'date_posted' (Latest)",
|
||||
"author_job_title": "Filter by job title of author (e.g., CEO)",
|
||||
"content_type": "Type of content post contains (photos, videos, liveVideos, collaborativeArticles, documents)",
|
||||
"from_member": "URN of person who posted (comma-separated for multiple)",
|
||||
"from_organization": "ID of organization who posted (comma-separated for multiple)",
|
||||
"author_company": "ID of company author works for (comma-separated for multiple)",
|
||||
"author_industry": "URN of industry author is connected with (comma-separated for multiple)",
|
||||
"mentions_member": "URN of person mentioned in post (comma-separated for multiple)",
|
||||
"mentions_organization": "ID of organization mentioned in post (comma-separated for multiple)"
|
||||
}
|
||||
},
|
||||
"search_jobs": {
|
||||
"route": "/search_jobs",
|
||||
"method": "GET",
|
||||
"name": "Search Jobs",
|
||||
"description": "Searches LinkedIn jobs with various filtering options",
|
||||
"payload": {
|
||||
"query": "Job search keywords (e.g., Software developer)",
|
||||
"page": "Page number",
|
||||
"searchLocationId": "Location ID for job search (get from Suggestion location endpoint)",
|
||||
"easyApply": "Filter for easy apply jobs (true or false)",
|
||||
"experience": "Experience level required (1=Internship, 2=Entry level, 3=Associate, 4=Mid senior, 5=Director, 6=Executive, comma-separated)",
|
||||
"jobType": "Job type (F=Full time, P=Part time, C=Contract, T=Temporary, V=Volunteer, I=Internship, O=Other, comma-separated)",
|
||||
"postedAgo": "Time jobs were posted in seconds (e.g., 3600 for past hour)",
|
||||
"workplaceType": "Workplace type (1=On-Site, 2=Remote, 3=Hybrid, comma-separated)",
|
||||
"sortBy": "Sort method (DD=most recent, R=most relevant)",
|
||||
"companyIdsList": "List of company IDs, comma-separated",
|
||||
"industryIdsList": "List of industry IDs, comma-separated",
|
||||
"functionIdsList": "List of function IDs, comma-separated",
|
||||
"titleIdsList": "List of job title IDs, comma-separated",
|
||||
"locationIdsList": "List of location IDs within specified searchLocationId country, comma-separated"
|
||||
}
|
||||
},
|
||||
"search_people_with_filters": {
|
||||
"route": "/search_people_with_filters",
|
||||
"method": "POST",
|
||||
"name": "Search People With Filters",
|
||||
"description": "Searches LinkedIn profiles with detailed filtering options",
|
||||
"payload": {
|
||||
"keyword": "General search keyword",
|
||||
"page": "Page number",
|
||||
"title_free_text": "Job title to filter by (e.g., CEO)",
|
||||
"company_free_text": "Company name to filter by",
|
||||
"first_name": "First name of person",
|
||||
"last_name": "Last name of person",
|
||||
"current_company_list": "List of current companies (comma-separated IDs)",
|
||||
"past_company_list": "List of past companies (comma-separated IDs)",
|
||||
"location_list": "List of locations (comma-separated IDs)",
|
||||
"language_list": "List of languages (comma-separated)",
|
||||
"service_catagory_list": "List of service categories (comma-separated)",
|
||||
"school_free_text": "School name to filter by",
|
||||
"industry_list": "List of industries (comma-separated IDs)",
|
||||
"school_list": "List of schools (comma-separated IDs)"
|
||||
}
|
||||
},
|
||||
"search_company_with_filters": {
|
||||
"route": "/search_company_with_filters",
|
||||
"method": "POST",
|
||||
"name": "Search Company With Filters",
|
||||
"description": "Searches LinkedIn companies with detailed filtering options",
|
||||
"payload": {
|
||||
"keyword": "General search keyword",
|
||||
"page": "Page number",
|
||||
"company_size_list": "List of company sizes (comma-separated, e.g., A,D)",
|
||||
"hasJobs": "Filter companies with jobs (true or false)",
|
||||
"location_list": "List of location IDs (comma-separated)",
|
||||
"industry_list": "List of industry IDs (comma-separated)"
|
||||
}
|
||||
}
|
||||
}
|
||||
base_url = "https://linkedin-data-scraper.p.rapidapi.com"
|
||||
super().__init__(base_url, endpoints)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
os.environ["RAPID_API_KEY"] = ""
|
||||
tool = LinkedInService()
|
||||
|
||||
result = tool.call_endpoint(
|
||||
route="comments_from_recent_activity",
|
||||
payload={"profile_url": "https://www.linkedin.com/in/adamcohenhillel/", "page": 1}
|
||||
)
|
||||
print(result)
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema
|
||||
from agent.tools.api_services.LinkedInService import LinkedInService
|
||||
|
||||
class APIServicesTool(Tool):
|
||||
"""Tool for making requests to various API services."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.register_apis = {
|
||||
"linkedin": LinkedInService()
|
||||
}
|
||||
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_api_service_endpoints",
|
||||
"description": "Get available endpoints for a specific API service",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the API service (e.g., 'linkedin')"
|
||||
}
|
||||
},
|
||||
"required": ["service_name"]
|
||||
}
|
||||
}
|
||||
})
|
||||
@xml_schema(
|
||||
tag_name="get-api-service-endpoints",
|
||||
mappings=[
|
||||
{"param_name": "service_name", "node_type": "attribute", "path": "."}
|
||||
],
|
||||
example='''
|
||||
<!--
|
||||
The get-api-service-endpoints tool returns available endpoints for a specific API service.
|
||||
Use this tool when you need to discover what endpoints are available.
|
||||
-->
|
||||
|
||||
<!-- Example to get LinkedIn API endpoints -->
|
||||
<get-api-service-endpoints service_name="linkedin">
|
||||
</get-api-service-endpoints>
|
||||
'''
|
||||
)
|
||||
async def get_api_service_endpoints(
|
||||
self,
|
||||
service_name: str
|
||||
) -> ToolResult:
|
||||
"""
|
||||
Get available endpoints for a specific API service.
|
||||
|
||||
Parameters:
|
||||
- service_name: The name of the API service (e.g., 'linkedin')
|
||||
"""
|
||||
try:
|
||||
if not service_name:
|
||||
return self.fail_response("API name is required.")
|
||||
|
||||
if service_name not in self.register_apis:
|
||||
return self.fail_response(f"API '{service_name}' not found. Available APIs: {list(self.register_apis.keys())}")
|
||||
|
||||
endpoints = self.register_apis[service_name].get_endpoints()
|
||||
return self.success_response(endpoints)
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
simplified_message = f"Error getting API endpoints: {error_message[:200]}"
|
||||
if len(error_message) > 200:
|
||||
simplified_message += "..."
|
||||
return self.fail_response(simplified_message)
|
||||
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "execute_api_call",
|
||||
"description": "Execute a call to a specific API endpoint",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the API service (e.g., 'linkedin')"
|
||||
},
|
||||
"route": {
|
||||
"type": "string",
|
||||
"description": "The key of the endpoint to call"
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"description": "The payload to send with the API call"
|
||||
}
|
||||
},
|
||||
"required": ["service_name", "route"]
|
||||
}
|
||||
}
|
||||
})
|
||||
@xml_schema(
|
||||
tag_name="execute-api-call",
|
||||
mappings=[
|
||||
{"param_name": "service_name", "node_type": "attribute", "path": "service_name"},
|
||||
{"param_name": "route", "node_type": "attribute", "path": "route"},
|
||||
{"param_name": "payload", "node_type": "content", "path": "."}
|
||||
],
|
||||
example='''
|
||||
<!--
|
||||
The execute-api-call tool makes a request to a specific API endpoint.
|
||||
Use this tool when you need to call an API endpoint with specific parameters.
|
||||
The route must be a valid endpoint key obtained from get-api-service-endpoints tool!!
|
||||
-->
|
||||
|
||||
<!-- Example to call linkedIn service with the specific route person -->
|
||||
<execute-api-call service_name="linkedin" route="person">
|
||||
{"link": "https://www.linkedin.com/in/johndoe/"}
|
||||
</execute-api-call>
|
||||
'''
|
||||
)
|
||||
async def execute_api_call(
|
||||
self,
|
||||
service_name: str,
|
||||
route: str,
|
||||
payload: str # this actually a json string
|
||||
) -> ToolResult:
|
||||
"""
|
||||
Execute a call to a specific API endpoint.
|
||||
|
||||
Parameters:
|
||||
- service_name: The name of the API service (e.g., 'linkedin')
|
||||
- route: The key of the endpoint to call
|
||||
- payload: The payload to send with the API call
|
||||
"""
|
||||
try:
|
||||
payload = json.loads(payload)
|
||||
|
||||
if not service_name:
|
||||
return self.fail_response("service_name is required.")
|
||||
|
||||
if not route:
|
||||
return self.fail_response("route is required.")
|
||||
|
||||
if service_name not in self.register_apis:
|
||||
return self.fail_response(f"API '{service_name}' not found. Available APIs: {list(self.register_apis.keys())}")
|
||||
|
||||
api_service = self.register_apis[service_name]
|
||||
if route == service_name:
|
||||
return self.fail_response(f"route '{route}' is the same as service_name '{service_name}'. YOU FUCKING IDIOT!")
|
||||
|
||||
if route not in api_service.get_endpoints().keys():
|
||||
return self.fail_response(f"Endpoint '{route}' not found in {service_name} API.")
|
||||
|
||||
|
||||
result = api_service.call_endpoint(route, payload)
|
||||
return self.success_response(result)
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
print(error_message)
|
||||
simplified_message = f"Error executing API call: {error_message[:200]}"
|
||||
if len(error_message) > 200:
|
||||
simplified_message += "..."
|
||||
return self.fail_response(simplified_message)
|
Loading…
Reference in New Issue