Merge pull request #38 from kortix-ai/rapid-api-base

Rapid api base, with LinkedIN
This commit is contained in:
Adam Cohen Hillel 2025-04-16 03:01:43 +01:00 committed by GitHub
commit a81c7db5a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 495 additions and 7 deletions

View File

@ -13,9 +13,9 @@ Suna's powerful toolkit includes seamless browser automation to navigate the web
[![License](https://img.shields.io/badge/License-Apache--2.0-blue)](./license)
[![Discord Follow](https://dcbadge.limes.pink/api/server/Py6pCBUUPw?style=flat)](https://discord.gg/Py6pCBUUPw)
[![Twitter Follow](https://img.shields.io/twitter/follow/kortixai)](https://x.com/kortixai)
[![GitHub Repo stars](https://img.shields.io/github/stars/kortix-ai/agentpress)](https://github.com/kortix-ai/agentpress)
[![Issues](https://img.shields.io/github/issues/kortix-ai/agentpress
)](https://github.com/kortix-ai/agentpress/labels/bug)
[![GitHub Repo stars](https://img.shields.io/github/stars/kortix-ai/suna)](https://github.com/kortix-ai/suna)
[![Issues](https://img.shields.io/github/issues/kortix-ai/suna
)](https://github.com/kortix-ai/suna/labels/bug)
</div>
@ -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**:

View File

@ -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():

View File

@ -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()

View File

@ -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)

View File

@ -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)