-
Notifications
You must be signed in to change notification settings - Fork 44.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(blocks): Add Hubspot blocks (#8786)
This PR adds the first few Hubspot blocks so we can create _real_ sales and marketing agents. ### Changes 🏗️ Added Hubspot blocks; - Aded auth for hubspot - Added Company block - Added Contact block - Added Engagement block ### Checklist 📋 #### For code changes: - [ ] I have clearly listed my changes in the PR description - [ ] I have made a test plan - [ ] I have tested my changes according to the test plan: <!-- Put your test plan here: --> - [ ] ... <details> <summary>Example test plan</summary> - [ ] Create from scratch and execute an agent with at least 3 blocks - [ ] Import an agent from file upload, and confirm it executes correctly - [ ] Upload agent to marketplace - [ ] Import an agent from marketplace and confirm it executes correctly - [ ] Edit an agent from monitor, and confirm it executes correctly </details> #### For configuration changes: - [ ] `.env.example` is updated or already compatible with my changes - [ ] `docker-compose.yml` is updated or already compatible with my changes - [ ] I have included a list of my configuration changes in the PR description (under **Changes**) <details> <summary>Examples of configuration changes</summary> - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases </details>
- Loading branch information
1 parent
eeb5b4a
commit 29f177e
Showing
8 changed files
with
374 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from typing import Literal | ||
|
||
from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials | ||
from pydantic import SecretStr | ||
|
||
from backend.data.model import CredentialsField, CredentialsMetaInput | ||
|
||
HubSpotCredentials = APIKeyCredentials | ||
HubSpotCredentialsInput = CredentialsMetaInput[ | ||
Literal["hubspot"], | ||
Literal["api_key"], | ||
] | ||
|
||
|
||
def HubSpotCredentialsField() -> HubSpotCredentialsInput: | ||
"""Creates a HubSpot credentials input on a block.""" | ||
return CredentialsField( | ||
provider="hubspot", | ||
supported_credential_types={"api_key"}, | ||
description="The HubSpot integration requires an API Key.", | ||
) | ||
|
||
|
||
TEST_CREDENTIALS = APIKeyCredentials( | ||
id="01234567-89ab-cdef-0123-456789abcdef", | ||
provider="hubspot", | ||
api_key=SecretStr("mock-hubspot-api-key"), | ||
title="Mock HubSpot API key", | ||
expires_at=None, | ||
) | ||
|
||
TEST_CREDENTIALS_INPUT = { | ||
"provider": TEST_CREDENTIALS.provider, | ||
"id": TEST_CREDENTIALS.id, | ||
"type": TEST_CREDENTIALS.type, | ||
"title": TEST_CREDENTIALS.title, | ||
} |
106 changes: 106 additions & 0 deletions
106
autogpt_platform/backend/backend/blocks/hubspot/company.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
from backend.blocks.hubspot._auth import ( | ||
HubSpotCredentials, | ||
HubSpotCredentialsField, | ||
HubSpotCredentialsInput, | ||
) | ||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema | ||
from backend.data.model import SchemaField | ||
from backend.util.request import requests | ||
|
||
|
||
class HubSpotCompanyBlock(Block): | ||
class Input(BlockSchema): | ||
credentials: HubSpotCredentialsInput = HubSpotCredentialsField() | ||
operation: str = SchemaField( | ||
description="Operation to perform (create, update, get)", default="get" | ||
) | ||
company_data: dict = SchemaField( | ||
description="Company data for create/update operations", default={} | ||
) | ||
domain: str = SchemaField( | ||
description="Company domain for get/update operations", default="" | ||
) | ||
|
||
class Output(BlockSchema): | ||
company: dict = SchemaField(description="Company information") | ||
status: str = SchemaField(description="Operation status") | ||
|
||
def __init__(self): | ||
super().__init__( | ||
id="3ae02219-d540-47cd-9c78-3ad6c7d9820a", | ||
description="Manages HubSpot companies - create, update, and retrieve company information", | ||
categories={BlockCategory.CRM}, | ||
input_schema=HubSpotCompanyBlock.Input, | ||
output_schema=HubSpotCompanyBlock.Output, | ||
) | ||
|
||
def run( | ||
self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs | ||
) -> BlockOutput: | ||
base_url = "https://api.hubapi.com/crm/v3/objects/companies" | ||
headers = { | ||
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}", | ||
"Content-Type": "application/json", | ||
} | ||
|
||
if input_data.operation == "create": | ||
response = requests.post( | ||
base_url, headers=headers, json={"properties": input_data.company_data} | ||
) | ||
result = response.json() | ||
yield "company", result | ||
yield "status", "created" | ||
|
||
elif input_data.operation == "get": | ||
search_url = f"{base_url}/search" | ||
search_data = { | ||
"filterGroups": [ | ||
{ | ||
"filters": [ | ||
{ | ||
"propertyName": "domain", | ||
"operator": "EQ", | ||
"value": input_data.domain, | ||
} | ||
] | ||
} | ||
] | ||
} | ||
response = requests.post(search_url, headers=headers, json=search_data) | ||
result = response.json() | ||
yield "company", result.get("results", [{}])[0] | ||
yield "status", "retrieved" | ||
|
||
elif input_data.operation == "update": | ||
# First get company ID by domain | ||
search_response = requests.post( | ||
f"{base_url}/search", | ||
headers=headers, | ||
json={ | ||
"filterGroups": [ | ||
{ | ||
"filters": [ | ||
{ | ||
"propertyName": "domain", | ||
"operator": "EQ", | ||
"value": input_data.domain, | ||
} | ||
] | ||
} | ||
] | ||
}, | ||
) | ||
company_id = search_response.json().get("results", [{}])[0].get("id") | ||
|
||
if company_id: | ||
response = requests.patch( | ||
f"{base_url}/{company_id}", | ||
headers=headers, | ||
json={"properties": input_data.company_data}, | ||
) | ||
result = response.json() | ||
yield "company", result | ||
yield "status", "updated" | ||
else: | ||
yield "company", {} | ||
yield "status", "company_not_found" |
106 changes: 106 additions & 0 deletions
106
autogpt_platform/backend/backend/blocks/hubspot/contact.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
from backend.blocks.hubspot._auth import ( | ||
HubSpotCredentials, | ||
HubSpotCredentialsField, | ||
HubSpotCredentialsInput, | ||
) | ||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema | ||
from backend.data.model import SchemaField | ||
from backend.util.request import requests | ||
|
||
|
||
class HubSpotContactBlock(Block): | ||
class Input(BlockSchema): | ||
credentials: HubSpotCredentialsInput = HubSpotCredentialsField() | ||
operation: str = SchemaField( | ||
description="Operation to perform (create, update, get)", default="get" | ||
) | ||
contact_data: dict = SchemaField( | ||
description="Contact data for create/update operations", default={} | ||
) | ||
email: str = SchemaField( | ||
description="Email address for get/update operations", default="" | ||
) | ||
|
||
class Output(BlockSchema): | ||
contact: dict = SchemaField(description="Contact information") | ||
status: str = SchemaField(description="Operation status") | ||
|
||
def __init__(self): | ||
super().__init__( | ||
id="5267326e-c4c1-4016-9f54-4e72ad02f813", | ||
description="Manages HubSpot contacts - create, update, and retrieve contact information", | ||
categories={BlockCategory.CRM}, | ||
input_schema=HubSpotContactBlock.Input, | ||
output_schema=HubSpotContactBlock.Output, | ||
) | ||
|
||
def run( | ||
self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs | ||
) -> BlockOutput: | ||
base_url = "https://api.hubapi.com/crm/v3/objects/contacts" | ||
headers = { | ||
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}", | ||
"Content-Type": "application/json", | ||
} | ||
|
||
if input_data.operation == "create": | ||
response = requests.post( | ||
base_url, headers=headers, json={"properties": input_data.contact_data} | ||
) | ||
result = response.json() | ||
yield "contact", result | ||
yield "status", "created" | ||
|
||
elif input_data.operation == "get": | ||
# Search for contact by email | ||
search_url = f"{base_url}/search" | ||
search_data = { | ||
"filterGroups": [ | ||
{ | ||
"filters": [ | ||
{ | ||
"propertyName": "email", | ||
"operator": "EQ", | ||
"value": input_data.email, | ||
} | ||
] | ||
} | ||
] | ||
} | ||
response = requests.post(search_url, headers=headers, json=search_data) | ||
result = response.json() | ||
yield "contact", result.get("results", [{}])[0] | ||
yield "status", "retrieved" | ||
|
||
elif input_data.operation == "update": | ||
search_response = requests.post( | ||
f"{base_url}/search", | ||
headers=headers, | ||
json={ | ||
"filterGroups": [ | ||
{ | ||
"filters": [ | ||
{ | ||
"propertyName": "email", | ||
"operator": "EQ", | ||
"value": input_data.email, | ||
} | ||
] | ||
} | ||
] | ||
}, | ||
) | ||
contact_id = search_response.json().get("results", [{}])[0].get("id") | ||
|
||
if contact_id: | ||
response = requests.patch( | ||
f"{base_url}/{contact_id}", | ||
headers=headers, | ||
json={"properties": input_data.contact_data}, | ||
) | ||
result = response.json() | ||
yield "contact", result | ||
yield "status", "updated" | ||
else: | ||
yield "contact", {} | ||
yield "status", "contact_not_found" |
121 changes: 121 additions & 0 deletions
121
autogpt_platform/backend/backend/blocks/hubspot/engagement.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
from datetime import datetime, timedelta | ||
|
||
from backend.blocks.hubspot._auth import ( | ||
HubSpotCredentials, | ||
HubSpotCredentialsField, | ||
HubSpotCredentialsInput, | ||
) | ||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema | ||
from backend.data.model import SchemaField | ||
from backend.util.request import requests | ||
|
||
|
||
class HubSpotEngagementBlock(Block): | ||
class Input(BlockSchema): | ||
credentials: HubSpotCredentialsInput = HubSpotCredentialsField() | ||
operation: str = SchemaField( | ||
description="Operation to perform (send_email, track_engagement)", | ||
default="send_email", | ||
) | ||
email_data: dict = SchemaField( | ||
description="Email data including recipient, subject, content", | ||
default={}, | ||
) | ||
contact_id: str = SchemaField( | ||
description="Contact ID for engagement tracking", default="" | ||
) | ||
timeframe_days: int = SchemaField( | ||
description="Number of days to look back for engagement", | ||
default=30, | ||
optional=True, | ||
) | ||
|
||
class Output(BlockSchema): | ||
result: dict = SchemaField(description="Operation result") | ||
status: str = SchemaField(description="Operation status") | ||
|
||
def __init__(self): | ||
super().__init__( | ||
id="c6524385-7d87-49d6-a470-248bd29ca765", | ||
description="Manages HubSpot engagements - sends emails and tracks engagement metrics", | ||
categories={BlockCategory.CRM, BlockCategory.COMMUNICATION}, | ||
input_schema=HubSpotEngagementBlock.Input, | ||
output_schema=HubSpotEngagementBlock.Output, | ||
) | ||
|
||
def run( | ||
self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs | ||
) -> BlockOutput: | ||
base_url = "https://api.hubapi.com" | ||
headers = { | ||
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}", | ||
"Content-Type": "application/json", | ||
} | ||
|
||
if input_data.operation == "send_email": | ||
# Using the email send API | ||
email_url = f"{base_url}/crm/v3/objects/emails" | ||
email_data = { | ||
"properties": { | ||
"hs_timestamp": datetime.now().isoformat(), | ||
"hubspot_owner_id": "1", # This should be configurable | ||
"hs_email_direction": "OUTBOUND", | ||
"hs_email_status": "SEND", | ||
"hs_email_subject": input_data.email_data.get("subject"), | ||
"hs_email_text": input_data.email_data.get("content"), | ||
"hs_email_to_email": input_data.email_data.get("recipient"), | ||
} | ||
} | ||
|
||
response = requests.post(email_url, headers=headers, json=email_data) | ||
result = response.json() | ||
yield "result", result | ||
yield "status", "email_sent" | ||
|
||
elif input_data.operation == "track_engagement": | ||
# Get engagement events for the contact | ||
from_date = datetime.now() - timedelta(days=input_data.timeframe_days) | ||
engagement_url = ( | ||
f"{base_url}/crm/v3/objects/contacts/{input_data.contact_id}/engagement" | ||
) | ||
|
||
params = {"limit": 100, "after": from_date.isoformat()} | ||
|
||
response = requests.get(engagement_url, headers=headers, params=params) | ||
engagements = response.json() | ||
|
||
# Process engagement metrics | ||
metrics = { | ||
"email_opens": 0, | ||
"email_clicks": 0, | ||
"email_replies": 0, | ||
"last_engagement": None, | ||
"engagement_score": 0, | ||
} | ||
|
||
for engagement in engagements.get("results", []): | ||
eng_type = engagement.get("properties", {}).get("hs_engagement_type") | ||
if eng_type == "EMAIL": | ||
metrics["email_opens"] += 1 | ||
elif eng_type == "EMAIL_CLICK": | ||
metrics["email_clicks"] += 1 | ||
elif eng_type == "EMAIL_REPLY": | ||
metrics["email_replies"] += 1 | ||
|
||
# Update last engagement time | ||
eng_time = engagement.get("properties", {}).get("hs_timestamp") | ||
if eng_time and ( | ||
not metrics["last_engagement"] | ||
or eng_time > metrics["last_engagement"] | ||
): | ||
metrics["last_engagement"] = eng_time | ||
|
||
# Calculate simple engagement score | ||
metrics["engagement_score"] = ( | ||
metrics["email_opens"] | ||
+ metrics["email_clicks"] * 2 | ||
+ metrics["email_replies"] * 3 | ||
) | ||
|
||
yield "result", metrics | ||
yield "status", "engagement_tracked" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.