diff --git a/README.md b/README.md
index ccfdf6ec..d22ad3ce 100644
--- a/README.md
+++ b/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)
@@ -135,13 +135,21 @@ 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**:
diff --git a/backend/agent/run.py b/backend/agent/run.py
index 5004c6c1..0cb1146b 100644
--- a/backend/agent/run.py
+++ b/backend/agent/run.py
@@ -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()
@@ -72,6 +72,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():
diff --git a/backend/agent/tools/api_services/APIServicesBase.py b/backend/agent/tools/api_services/APIServicesBase.py
new file mode 100644
index 00000000..7ea9c9b1
--- /dev/null
+++ b/backend/agent/tools/api_services/APIServicesBase.py
@@ -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()
diff --git a/backend/agent/tools/api_services/LinkedInService.py b/backend/agent/tools/api_services/LinkedInService.py
new file mode 100644
index 00000000..adb483db
--- /dev/null
+++ b/backend/agent/tools/api_services/LinkedInService.py
@@ -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)
+
diff --git a/backend/agent/tools/api_services_tool.py b/backend/agent/tools/api_services_tool.py
new file mode 100644
index 00000000..9afdf0bb
--- /dev/null
+++ b/backend/agent/tools/api_services_tool.py
@@ -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='''
+
+
+
+
+
+ '''
+ )
+ 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='''
+
+
+
+
+ {"link": "https://www.linkedin.com/in/johndoe/"}
+
+ '''
+ )
+ 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)