From 960bd2d2999912dbf2a078d6e278627f3269758c Mon Sep 17 00:00:00 2001 From: rcmohan Date: Sat, 3 May 2025 08:18:47 -0400 Subject: [PATCH 01/11] HubSpot agent - first commit --- level_1/hubspot_agent/hubspot/.env | 2 + level_1/hubspot_agent/hubspot/Secrets.py | 87 ++++++++ level_1/hubspot_agent/hubspot/__init__.py | 1 + level_1/hubspot_agent/hubspot/agent.py | 51 +++++ level_1/hubspot_agent/hubspot/config.py | 19 ++ level_1/hubspot_agent/hubspot/contacts.csv | 197 ++++++++++++++++++ level_1/hubspot_agent/hubspot/hubspot_tool.py | 151 ++++++++++++++ .../hubspot_agent/hubspot/postgres_client.py | 125 +++++++++++ level_1/hubspot_agent/requirements.txt | 8 + 9 files changed, 641 insertions(+) create mode 100644 level_1/hubspot_agent/hubspot/.env create mode 100644 level_1/hubspot_agent/hubspot/Secrets.py create mode 100644 level_1/hubspot_agent/hubspot/__init__.py create mode 100644 level_1/hubspot_agent/hubspot/agent.py create mode 100644 level_1/hubspot_agent/hubspot/config.py create mode 100644 level_1/hubspot_agent/hubspot/contacts.csv create mode 100644 level_1/hubspot_agent/hubspot/hubspot_tool.py create mode 100644 level_1/hubspot_agent/hubspot/postgres_client.py create mode 100644 level_1/hubspot_agent/requirements.txt diff --git a/level_1/hubspot_agent/hubspot/.env b/level_1/hubspot_agent/hubspot/.env new file mode 100644 index 0000000..b17d7c2 --- /dev/null +++ b/level_1/hubspot_agent/hubspot/.env @@ -0,0 +1,2 @@ +GOOGLE_GENAI_USE_VERTEXAI=FALSE +GOOGLE_API_KEY=AIzaS... \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/Secrets.py b/level_1/hubspot_agent/hubspot/Secrets.py new file mode 100644 index 0000000..8bb3c58 --- /dev/null +++ b/level_1/hubspot_agent/hubspot/Secrets.py @@ -0,0 +1,87 @@ + +# !/usr/bin/env python3 +""" +Setup script to initialize keyring with necessary credentials +Run this script once to set up all required credentials securely +""" + +import keyring +import getpass +from keyring.backends import Windows +from config import Config + +class Setup: + def __init__(self): + windows_backend = Windows.WinVaultKeyring() + keyring.set_keyring(windows_backend) + + def setup_credentials(self): + print("Setting up secure credentials for HubSpot integration") + print("=" * 50) + + # Get service ID from config + service_id = Config.hubspot["service_id"] + + # Get HubSpot access token + print("\nHubSpot API Access Token") + print("-" * 30) + access_token = getpass.getpass("Enter your HubSpot access token: ") + keyring.set_password(service_id, Config.hubspot["access_token_key"], access_token) + + + # Get database credentials + print("\nDatabase Credentials") + print("-" * 30) + print(f"Database Name: {Config.database['name']} (from config file)") + print(f"Database Host: {Config.database['host']} (from config file)") + print(f"Database Port: {Config.database['port']} (from config file)") + + db_user = input("Enter database username: ") + db_password = getpass.getpass("Enter database password: ") + + # Store credentials in keyring + keyring.set_password(service_id, Config.hubspot["db_user_key"], db_user) + keyring.set_password(service_id, Config.hubspot["db_password_key"], db_password) + + print("\nCredentials stored successfully!") + print("You can now run the main program.") + + + def test_credentials(self): + """Test if credentials are properly set in keyring""" + service_id = Config.hubspot["service_id"] + + # Test HubSpot access token + access_token = keyring.get_password(service_id, Config.hubspot["access_token_key"]) + if not access_token: + return False, "HubSpot access token not found" + + # Test database credentials + db_user = keyring.get_password(service_id, Config.hubspot["db_user_key"]) + if not db_user: + return False, "Database username not found" + # else: + # print(db_user) + + db_password = keyring.get_password(service_id, Config.hubspot["db_password_key"]) + if not db_password: + return False, "Database password not found" + + return True, "All credentials found" + + +if __name__ == "__main__": + setup = Setup() + # Check if credentials already exist + success, message = setup.test_credentials() + + if success: + print("Credentials already exist in keyring.") + choice = input("Do you want to reset credentials? (y/n): ").lower() + if choice == 'y': + setup.setup_credentials() + else: + print("Keeping existing credentials.") + else: + print(f"Missing credentials: {message}") + setup.setup_credentials() diff --git a/level_1/hubspot_agent/hubspot/__init__.py b/level_1/hubspot_agent/hubspot/__init__.py new file mode 100644 index 0000000..02c597e --- /dev/null +++ b/level_1/hubspot_agent/hubspot/__init__.py @@ -0,0 +1 @@ +from . import agent diff --git a/level_1/hubspot_agent/hubspot/agent.py b/level_1/hubspot_agent/hubspot/agent.py new file mode 100644 index 0000000..806f664 --- /dev/null +++ b/level_1/hubspot_agent/hubspot/agent.py @@ -0,0 +1,51 @@ +from hubspot.hubspot_tool import create_lead +from google.adk.agents import Agent + +root_agent = Agent( + name="hubspot_agent", + model="gemini-2.0-flash", + description=( + "Agent to manage leads in HubSpot" + ), + instruction=( + """ + You are a HubSpot Agent designed to help administrators create and manage leads, and setup meetings with + potential leads. + + Your capabilities: + 1. Read emails and extract data about the lead + 2. Call Hubspot APIs using HubSpotTool + + When you read an email: + 1. Identify the fields that match the list in the {contact.csv} file + 2. Create a json as in the following example: + { + "properties": { + "email": "new.lead.private.app@example.com", + "firstname": "App", + "lastname": "Lead", + "phone": "111-222-3333", + "company": "App Integrations Inc.", + "website": "https://www.appintegrations.com", + "lifecyclestage": "lead" + } + } + 3. Invoke the create_lead tool or modify_lead tool with the appropriate json. + + TOOLS AVAILABLE: + + create_lead: + - Description: Creates a new lead in HubSpot. Accepts a single parameter `json_payload` which must be a JSON object containing the lead's properties. + + Example workflow: + 1. Receive user request with email content + 2. Determine the json payload contents based on list of fields defined in contacts.csv + 3. Print the prepared json payload + 4. Call create_lead() tool, passing the prepared JSON object as the `json_payload` argument. + 5. Print the response + + Always think step-by-step about the most efficient and secure way to fulfill user requests. + """ + ), + tools=[create_lead] +) diff --git a/level_1/hubspot_agent/hubspot/config.py b/level_1/hubspot_agent/hubspot/config.py new file mode 100644 index 0000000..54aa8f7 --- /dev/null +++ b/level_1/hubspot_agent/hubspot/config.py @@ -0,0 +1,19 @@ +class Config: + def __init__(self): + pass + + # Database configuration + database = { + "name": "database_name", # Database name + "host": "localhost", # Database host + "port": 5432 # Database port + } + + # HubSpot configuration + hubspot = { + "base_url": "https://api.hubspot.com", + "service_id": "hubspot_service", # Service ID for keyring + "access_token_key": "access_token", # Key name for access token in keyring + "db_user_key": "db_user", # Key name for database username in keyring + "db_password_key": "db_password" # Key name for database password in keyring + } diff --git a/level_1/hubspot_agent/hubspot/contacts.csv b/level_1/hubspot_agent/hubspot/contacts.csv new file mode 100644 index 0000000..2a468d2 --- /dev/null +++ b/level_1/hubspot_agent/hubspot/contacts.csv @@ -0,0 +1,197 @@ +Name,Internal name,Type,Description,Group name,Form field,Options,Read only value,Read only definition,Calculated,External options,Deleted,Hubspot defined,Created user,Usages +Average Pageviews,hs_analytics_average_page_views,number,Average number of pageviews per session for this contact. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First Referring Site,hs_analytics_first_referrer,string,URL that referred the contact to your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Time First Seen,hs_analytics_first_timestamp,datetime,First time the contact has been seen. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First Touch Converting Campaign,hs_analytics_first_touch_converting_campaign,string,Campaign responsible for the first touch creation of this contact.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot, +First Page Seen,hs_analytics_first_url,string,First page the contact visited on your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Time of First Session,hs_analytics_first_visit_timestamp,datetime,First time the contact visited your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last Referring Site,hs_analytics_last_referrer,string,Last URL that referred contact to your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Time Last Seen,hs_analytics_last_timestamp,datetime,Timestamp for most recent webpage view on your website.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last Touch Converting Campaign,hs_analytics_last_touch_converting_campaign,string,Campaign responsible for the last touch creation of this contact.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot, +Last Page Seen,hs_analytics_last_url,string,Last page the contact visited on your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Time of Last Session,hs_analytics_last_visit_timestamp,datetime,Timestamp for start of the most recent session for this contact to your website.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of event completions,hs_analytics_num_event_completions,number,Total number of events for this contact. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of Pageviews,hs_analytics_num_page_views,number,Total number of page views this contact has had on your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of Sessions,hs_analytics_num_visits,number,Number of times a contact has come to your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Event Revenue,hs_analytics_revenue,number,Set event revenue on a contact though the Enterprise Events feature. http://help.hubspot.com/articles/KCS_Article/Reports/How-do-I-create-Events-in-HubSpot,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Original Traffic Source,hs_analytics_source,enumeration,"First known source the contact used to find your website. Set automatically, but may be updated manually.",analyticsinformation,FALSE,"[{""label"":""Organic Search"",""value"":""ORGANIC_SEARCH"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Paid Search"",""value"":""PAID_SEARCH"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Email Marketing"",""value"":""EMAIL_MARKETING"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Organic Social"",""value"":""SOCIAL_MEDIA"",""displayOrder"":3,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Referrals"",""value"":""REFERRALS"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Other Campaigns"",""value"":""OTHER_CAMPAIGNS"",""displayOrder"":5,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Direct Traffic"",""value"":""DIRECT_TRAFFIC"",""displayOrder"":6,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Offline Sources"",""value"":""OFFLINE"",""displayOrder"":7,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Paid Social"",""value"":""PAID_SOCIAL"",""displayOrder"":8,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Original Traffic Source Drill-Down 1,hs_analytics_source_data_1,string,Additional information about the source the contact used to find your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Original Traffic Source Drill-Down 2,hs_analytics_source_data_2,string,Additional information about the source the contact used to find your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Latest Traffic Source,hs_latest_source,enumeration,The source of the latest session for a contact,analyticsinformation,FALSE,"[{""label"":""Organic Search"",""value"":""ORGANIC_SEARCH"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Paid Search"",""value"":""PAID_SEARCH"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Email Marketing"",""value"":""EMAIL_MARKETING"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Organic Social"",""value"":""SOCIAL_MEDIA"",""displayOrder"":3,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Referrals"",""value"":""REFERRALS"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Other Campaigns"",""value"":""OTHER_CAMPAIGNS"",""displayOrder"":5,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Direct Traffic"",""value"":""DIRECT_TRAFFIC"",""displayOrder"":6,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Offline Sources"",""value"":""OFFLINE"",""displayOrder"":7,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Paid Social"",""value"":""PAID_SOCIAL"",""displayOrder"":8,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Latest Traffic Source Drill-Down 1,hs_latest_source_data_1,string,Additional information about the latest source for the last session the contact used to find your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Latest Traffic Source Drill-Down 2,hs_latest_source_data_2,string,Additional information about the source for the last session the contact used to find your website. Set automatically.,analyticsinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Latest Traffic Source Date,hs_latest_source_timestamp,datetime,The time of the latest session for a contact,analyticsinformation,FALSE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Date of last meeting booked in meetings tool,engagements_last_meeting_booked,datetime,"The date of the last meeting that has been scheduled by a contact through the meetings tool. If multiple meetings have been scheduled, the date of the last chronological meeting in the timeline is shown.",contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Campaign of last booking in meetings tool,engagements_last_meeting_booked_campaign,string,UTM parameter for marketing campaign (e.g. a specific email) responsible for recent meeting booking. Only populated when tracking parameters are included in meeting link.,contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Medium of last booking in meetings tool,engagements_last_meeting_booked_medium,string,UTM parameter for the channel (e.g. email) responsible for most recent meeting booking. Only populated when tracking parameters are included in meeting link.,contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Source of last booking in meetings tool,engagements_last_meeting_booked_source,string,UTM parameter for the site (e.g. Twitter) responsible for most recent meeting booking. Only populated when tracking parameters are included in meeting link.,contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First Conversion Date,first_conversion_date,datetime,Date this contact first submitted a form.,contact_activity,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +First Conversion,first_conversion_event_name,string,First form this contact submitted.,contact_activity,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Membership Notes,hs_content_membership_notes,string,Notes relating to the contact's content membership.,contact_activity,FALSE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Status,hs_content_membership_status,enumeration,Status of the contact's content membership.,contact_activity,FALSE,"[{""label"":""Active"",""value"":""active"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Inactive"",""value"":""inactive"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last Engagement Date,hs_last_sales_activity_timestamp,datetime,"The last time a contact engaged with your site or a form, document, meetings link, or tracked email. This doesn't include marketing emails or emails to multiple contacts.",contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Recent Sales Email Clicked Date,hs_sales_email_last_clicked,datetime,The last time a tracked sales email was clicked by this user,contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Recent Sales Email Opened Date,hs_sales_email_last_opened,datetime,The last time a tracked sales email was opened by this contact. This property does not update for emails that were sent to more than one contact.,contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Recent Sales Email Replied Date,hs_sales_email_last_replied,datetime,The last time a tracked sales email to this contact was replied to. This is set automatically by HubSpot.,contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Currently in Sequence,hs_sequences_is_enrolled,bool,A yes/no field that indicates whether the contact is currently in a Sequence.,contact_activity,FALSE,"[{""label"":""True"",""value"":""true"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""False"",""value"":""false"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Message,message,string,A default property to be used for any message or comments a contact may want to leave on a form.,contact_activity,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last Contacted,notes_last_contacted,datetime,"The last time a call, chat conversation, LinkedIn message, postal mail, meeting, sales email, SMS, or WhatsApp message was logged for a contact. This is set automatically by HubSpot based on user actions in the contact record.",contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot,Properties v3 card (1) +Last Activity Date,notes_last_updated,datetime,"The last time a call, chat conversation, LinkedIn message, postal mail, meeting, note, sales email, SMS, or WhatsApp message was logged for a contact. This is set automatically by HubSpot based on user actions in the contact record.",contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Next Activity Date,notes_next_activity_date,datetime,The date of the next upcoming activity for a contact. This is set automatically by HubSpot based on user actions in the contact record.,contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of times contacted,num_contacted_notes,number,"The number of times a call, chat conversation, LinkedIn message, postal mail, meeting, sales email, SMS, or WhatsApp message was logged for a contact record. This is set automatically by HubSpot based on user actions in the contact record.",contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of Sales Activities,num_notes,number,"The number of times a call, chat conversation, LinkedIn message, postal mail, meeting, note, sales email, SMS, task, or WhatsApp message was logged for a contact record. This is set automatically by HubSpot based on user actions in the contact record.",contact_activity,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Street Address,address,string,"Contact's street address, including apartment or unit number.",contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Annual Revenue,annualrevenue,string,Annual company revenue,contactinformation,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Budget,budget,number,,contactinformation,FALSE,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,Unknown user,Sales view (3); Properties v3 card (1) +City,city,string,A contact's city of residence,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Company Name,company,string,Name of the contact's company. This can be set independently from the name property on the contact's associated company.,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Country/Region,country,string,"The contact's country/region of residence. This might be set via import, form, or integration.",contactinformation,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Create Date,createdate,datetime,The date that a contact entered the system,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Email,email,string,A contact's email address,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot,Sales view (3); Properties v3 card (1) +Fax Number,fax,string,A contact's primary fax number,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First Name,firstname,string,A contact's first name,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Enrichment opt out,hs_contact_enrichment_opt_out,bool,,contactinformation,FALSE,"[{""label"":""Yes"",""value"":""true"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""No"",""value"":""false"",""displayOrder"":1,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Enrichment opt out timestamp,hs_contact_enrichment_opt_out_timestamp,datetime,Timestamp of when the contact opted out of being included in the HubSpot Enrichment database,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Member email,hs_content_membership_email,string,Email used to send private content information to members,contactinformation,FALSE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Email Confirmed,hs_content_membership_email_confirmed,bool,Email Confirmation status of user of Content Membership,contactinformation,FALSE,"[{""label"":""True"",""value"":""true"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""False"",""value"":""false"",""displayOrder"":1,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Time enrolled in registration follow up emails,hs_content_membership_follow_up_enqueued_at,datetime,The time when the contact was first enrolled in the registration follow up email flow,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Registered At,hs_content_membership_registered_at,datetime,Datetime at which this user was set up for Content Membership,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Domain to which registration email was sent,hs_content_membership_registration_domain_sent_to,string,Domain to which the registration invitation email for Content Membership was sent to,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Time registration email was sent,hs_content_membership_registration_email_sent_at,datetime,Datetime at which this user was sent a registration invitation email for Content Membership,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Country/Region Code,hs_country_region_code,string,The contact's two-letter country code.,contactinformation,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Created by user ID,hs_created_by_user_id,number,The user who created this record. This value is set automatically by HubSpot.,contactinformation,FALSE,,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot, +Email Domain,hs_email_domain,string,A contact's email address domain,contactinformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +ID of first engagement,hs_first_engagement_object_id,number,The object id of the current contact owner's first engagement with the contact.,contactinformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Has been enriched,hs_is_enriched,bool,Indicates whether this object has ever had enriched properties written to it.,contactinformation,FALSE,"[{""label"":""Yes"",""value"":""true"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""No"",""value"":""false"",""displayOrder"":1,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Preferred language,hs_language,enumeration,"Set your contact's preferred language for communications. This property can be changed from an import, form, or integration.",contactinformation,TRUE,"[{""label"":""Afrikaans"",""value"":""af"",""displayOrder"":0,""hidden"":false,""readOnly"":true},{""label"":""Albanian"",""value"":""sq"",""displayOrder"":1,""hidden"":false,""readOnly"":true},{""label"":""Albanian - Albania"",""value"":""sq-al"",""displayOrder"":2,""hidden"":false,""readOnly"":true},{""label"":""Arabic"",""value"":""ar"",""displayOrder"":3,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Algeria"",""value"":""ar-dz"",""displayOrder"":4,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Bahrain"",""value"":""ar-bh"",""displayOrder"":5,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Egypt"",""value"":""ar-eg"",""displayOrder"":6,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Iraq"",""value"":""ar-iq"",""displayOrder"":7,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Jordan"",""value"":""ar-jo"",""displayOrder"":8,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Kuwait"",""value"":""ar-kw"",""displayOrder"":9,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Lebanon"",""value"":""ar-lb"",""displayOrder"":10,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Libya"",""value"":""ar-ly"",""displayOrder"":11,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Morocco"",""value"":""ar-ma"",""displayOrder"":12,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Oman"",""value"":""ar-om"",""displayOrder"":13,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Qatar"",""value"":""ar-qa"",""displayOrder"":14,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Saudi Arabia"",""value"":""ar-sa"",""displayOrder"":15,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Sudan"",""value"":""ar-sd"",""displayOrder"":16,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Syria"",""value"":""ar-sy"",""displayOrder"":17,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Tunisia"",""value"":""ar-tn"",""displayOrder"":18,""hidden"":false,""readOnly"":true},{""label"":""Arabic - United Arab Emirates"",""value"":""ar-ae"",""displayOrder"":19,""hidden"":false,""readOnly"":true},{""label"":""Arabic - Yemen"",""value"":""ar-ye"",""displayOrder"":20,""hidden"":false,""readOnly"":true},{""label"":""Armenian"",""value"":""hy"",""displayOrder"":21,""hidden"":false,""readOnly"":true},{""label"":""Assamese"",""value"":""as"",""displayOrder"":22,""hidden"":false,""readOnly"":true},{""label"":""Azerbaijani"",""value"":""az"",""displayOrder"":23,""hidden"":false,""readOnly"":true},{""label"":""Basque"",""value"":""eu"",""displayOrder"":24,""hidden"":false,""readOnly"":true},{""label"":""Belarusian"",""value"":""be"",""displayOrder"":25,""hidden"":false,""readOnly"":true},{""label"":""Belarusian - Belarus"",""value"":""be-by"",""displayOrder"":26,""hidden"":false,""readOnly"":true},{""label"":""Bengali"",""value"":""bn"",""displayOrder"":27,""hidden"":false,""readOnly"":true},{""label"":""Bosnian"",""value"":""ba"",""displayOrder"":28,""hidden"":false,""readOnly"":false},{""label"":""Bulgarian"",""value"":""bg"",""displayOrder"":29,""hidden"":false,""readOnly"":true},{""label"":""Bulgarian - Bulgaria"",""value"":""bg-bg"",""displayOrder"":30,""hidden"":false,""readOnly"":true},{""label"":""Burmese"",""value"":""my"",""displayOrder"":31,""hidden"":false,""readOnly"":true},{""label"":""Burmese - Myanmar (Burma)"",""value"":""my-mm"",""displayOrder"":32,""hidden"":false,""readOnly"":true},{""label"":""Catalan"",""value"":""ca"",""displayOrder"":33,""hidden"":false,""readOnly"":true},{""label"":""Catalan - Catalan"",""value"":""ca-es"",""displayOrder"":34,""hidden"":false,""readOnly"":true},{""label"":""Cebuano - Philippines"",""value"":""cb-pl"",""displayOrder"":35,""hidden"":false,""readOnly"":false},{""label"":""Chinese"",""value"":""zh"",""displayOrder"":36,""hidden"":false,""readOnly"":true},{""label"":""Chinese - China"",""value"":""zh-cn"",""displayOrder"":37,""hidden"":false,""readOnly"":true},{""label"":""Chinese - Hong Kong SAR"",""value"":""zh-hk"",""displayOrder"":38,""hidden"":false,""readOnly"":true},{""label"":""Chinese - Macau SAR"",""value"":""zh-mo"",""displayOrder"":39,""hidden"":false,""readOnly"":true},{""label"":""Chinese - Singapore"",""value"":""zh-sg"",""displayOrder"":40,""hidden"":false,""readOnly"":true},{""label"":""Chinese - Taiwan"",""value"":""zh-tw"",""displayOrder"":41,""hidden"":false,""readOnly"":true},{""label"":""Chinese (Simplified)"",""value"":""zh-chs"",""displayOrder"":42,""hidden"":false,""readOnly"":true},{""label"":""Chinese (Traditional)"",""value"":""zh-cht"",""displayOrder"":43,""hidden"":false,""readOnly"":true},{""label"":""Croatian"",""value"":""hr"",""displayOrder"":44,""hidden"":false,""readOnly"":true},{""label"":""Croatian - Croatia"",""value"":""hr-hr"",""displayOrder"":45,""hidden"":false,""readOnly"":true},{""label"":""Czech"",""value"":""cs"",""displayOrder"":46,""hidden"":false,""readOnly"":true},{""label"":""Czech - Czech Republic"",""value"":""cs-cz"",""displayOrder"":47,""hidden"":false,""readOnly"":true},{""label"":""Danish"",""value"":""da"",""displayOrder"":48,""hidden"":false,""readOnly"":true},{""label"":""Danish - Denmark"",""value"":""da-dk"",""displayOrder"":49,""hidden"":false,""readOnly"":true},{""label"":""Dutch"",""value"":""nl"",""displayOrder"":50,""hidden"":false,""readOnly"":true},{""label"":""Dutch - Belgium"",""value"":""nl-be"",""displayOrder"":51,""hidden"":false,""readOnly"":true},{""label"":""Dutch - The Netherlands"",""value"":""nl-nl"",""displayOrder"":52,""hidden"":false,""readOnly"":true},{""label"":""English"",""value"":""en"",""displayOrder"":53,""hidden"":false,""readOnly"":true},{""label"":""English - Australia"",""value"":""en-au"",""displayOrder"":54,""hidden"":false,""readOnly"":true},{""label"":""English - Canada"",""value"":""en-ca"",""displayOrder"":55,""hidden"":false,""readOnly"":true},{""label"":""English - Hong Kong"",""value"":""en-hk"",""displayOrder"":56,""hidden"":false,""readOnly"":true},{""label"":""English - India"",""value"":""en-in"",""displayOrder"":57,""hidden"":false,""readOnly"":true},{""label"":""English - Ireland"",""value"":""en-ie"",""displayOrder"":58,""hidden"":false,""readOnly"":true},{""label"":""English - Malaysia"",""value"":""en-my"",""displayOrder"":59,""hidden"":false,""readOnly"":true},{""label"":""English - Malta"",""value"":""en-mt"",""displayOrder"":60,""hidden"":false,""readOnly"":true},{""label"":""English - New Zealand"",""value"":""en-nz"",""displayOrder"":61,""hidden"":false,""readOnly"":true},{""label"":""English - Philippines"",""value"":""en-ph"",""displayOrder"":62,""hidden"":false,""readOnly"":true},{""label"":""English - Singapore"",""value"":""en-sg"",""displayOrder"":63,""hidden"":false,""readOnly"":true},{""label"":""English - South Africa"",""value"":""en-za"",""displayOrder"":64,""hidden"":false,""readOnly"":true},{""label"":""English - United Kingdom"",""value"":""en-gb"",""displayOrder"":65,""hidden"":false,""readOnly"":true},{""label"":""English - United States"",""value"":""en-us"",""displayOrder"":66,""hidden"":false,""readOnly"":true},{""label"":""English - Zimbabwe"",""value"":""en-zw"",""displayOrder"":67,""hidden"":false,""readOnly"":true},{""label"":""Estonian"",""value"":""et"",""displayOrder"":68,""hidden"":false,""readOnly"":true},{""label"":""Estonian - Estonia"",""value"":""et-ee"",""displayOrder"":69,""hidden"":false,""readOnly"":true},{""label"":""Faroese"",""value"":""fo"",""displayOrder"":70,""hidden"":false,""readOnly"":true},{""label"":""Farsi"",""value"":""fa"",""displayOrder"":71,""hidden"":false,""readOnly"":true},{""label"":""Finnish"",""value"":""fi"",""displayOrder"":72,""hidden"":false,""readOnly"":true},{""label"":""Finnish - Finland"",""value"":""fi-fi"",""displayOrder"":73,""hidden"":false,""readOnly"":true},{""label"":""French"",""value"":""fr"",""displayOrder"":74,""hidden"":false,""readOnly"":true},{""label"":""French - Belgium"",""value"":""fr-be"",""displayOrder"":75,""hidden"":false,""readOnly"":true},{""label"":""French - Canada"",""value"":""fr-ca"",""displayOrder"":76,""hidden"":false,""readOnly"":true},{""label"":""French - France"",""value"":""fr-fr"",""displayOrder"":77,""hidden"":false,""readOnly"":true},{""label"":""French - Luxembourg"",""value"":""fr-lu"",""displayOrder"":78,""hidden"":false,""readOnly"":true},{""label"":""French - Monaco"",""value"":""fr-mc"",""displayOrder"":79,""hidden"":false,""readOnly"":true},{""label"":""French - Switzerland"",""value"":""fr-ch"",""displayOrder"":80,""hidden"":false,""readOnly"":true},{""label"":""Galician"",""value"":""gl"",""displayOrder"":81,""hidden"":false,""readOnly"":true},{""label"":""Georgian"",""value"":""ka"",""displayOrder"":82,""hidden"":false,""readOnly"":true},{""label"":""German"",""value"":""de"",""displayOrder"":83,""hidden"":false,""readOnly"":true},{""label"":""German - Austria"",""value"":""de-at"",""displayOrder"":84,""hidden"":false,""readOnly"":true},{""label"":""German - Germany"",""value"":""de-de"",""displayOrder"":85,""hidden"":false,""readOnly"":true},{""label"":""German - Greece"",""value"":""de-gr"",""displayOrder"":86,""hidden"":false,""readOnly"":true},{""label"":""German - Liechtenstein"",""value"":""de-li"",""displayOrder"":87,""hidden"":false,""readOnly"":true},{""label"":""German - Luxembourg"",""value"":""de-lu"",""displayOrder"":88,""hidden"":false,""readOnly"":true},{""label"":""German - Switzerland"",""value"":""de-ch"",""displayOrder"":89,""hidden"":false,""readOnly"":true},{""label"":""Greek"",""value"":""el"",""displayOrder"":90,""hidden"":false,""readOnly"":true},{""label"":""Greek - Cyprus"",""value"":""el-cy"",""displayOrder"":91,""hidden"":false,""readOnly"":true},{""label"":""Greek - Greece"",""value"":""el-gr"",""displayOrder"":92,""hidden"":false,""readOnly"":true},{""label"":""Gujarati"",""value"":""gu"",""displayOrder"":93,""hidden"":false,""readOnly"":true},{""label"":""Haitian Creole"",""value"":""ht"",""displayOrder"":94,""hidden"":false,""readOnly"":false},{""label"":""Hausa"",""value"":""ha"",""displayOrder"":95,""hidden"":false,""readOnly"":false},{""label"":""Hebrew"",""value"":""he"",""displayOrder"":96,""hidden"":false,""readOnly"":true},{""label"":""Hebrew - Israel"",""value"":""he-il"",""displayOrder"":97,""hidden"":false,""readOnly"":false},{""label"":""Hebrew - Israel (Legacy)"",""value"":""iw-il"",""displayOrder"":98,""hidden"":false,""readOnly"":true},{""label"":""Hindi"",""value"":""hi"",""displayOrder"":99,""hidden"":false,""readOnly"":true},{""label"":""Hindi - India"",""value"":""hi-in"",""displayOrder"":100,""hidden"":false,""readOnly"":true},{""label"":""Hungarian"",""value"":""hu"",""displayOrder"":101,""hidden"":false,""readOnly"":true},{""label"":""Hungarian - Hungary"",""value"":""hu-hu"",""displayOrder"":102,""hidden"":false,""readOnly"":true},{""label"":""Icelandic"",""value"":""is"",""displayOrder"":103,""hidden"":false,""readOnly"":true},{""label"":""Icelandic - Iceland"",""value"":""is-is"",""displayOrder"":104,""hidden"":false,""readOnly"":true},{""label"":""Indonesian"",""value"":""id"",""displayOrder"":105,""hidden"":false,""readOnly"":true},{""label"":""Indonesian - Indonesia"",""value"":""in-id"",""displayOrder"":106,""hidden"":false,""readOnly"":true},{""label"":""Irish"",""value"":""ga"",""displayOrder"":107,""hidden"":false,""readOnly"":true},{""label"":""Irish - Ireland"",""value"":""ga-ie"",""displayOrder"":108,""hidden"":false,""readOnly"":true},{""label"":""Italian"",""value"":""it"",""displayOrder"":109,""hidden"":false,""readOnly"":true},{""label"":""Italian - Italy"",""value"":""it-it"",""displayOrder"":110,""hidden"":false,""readOnly"":true},{""label"":""Italian - Switzerland"",""value"":""it-ch"",""displayOrder"":111,""hidden"":false,""readOnly"":true},{""label"":""Japanese"",""value"":""ja"",""displayOrder"":112,""hidden"":false,""readOnly"":true},{""label"":""Japanese - Japan"",""value"":""ja-jp"",""displayOrder"":113,""hidden"":false,""readOnly"":true},{""label"":""Kannada"",""value"":""kn"",""displayOrder"":114,""hidden"":false,""readOnly"":true},{""label"":""Kazakh"",""value"":""kk"",""displayOrder"":115,""hidden"":false,""readOnly"":true},{""label"":""Kinyarwanda"",""value"":""rw"",""displayOrder"":116,""hidden"":false,""readOnly"":false},{""label"":""Kiswahili"",""value"":""ki"",""displayOrder"":117,""hidden"":false,""readOnly"":false},{""label"":""Konkani"",""value"":""kok"",""displayOrder"":118,""hidden"":false,""readOnly"":true},{""label"":""Korean"",""value"":""ko"",""displayOrder"":119,""hidden"":false,""readOnly"":true},{""label"":""Korean - South Korea"",""value"":""ko-kr"",""displayOrder"":120,""hidden"":false,""readOnly"":true},{""label"":""Kurdish"",""value"":""ku"",""displayOrder"":121,""hidden"":false,""readOnly"":false},{""label"":""Kyrgyz"",""value"":""ky"",""displayOrder"":122,""hidden"":false,""readOnly"":true},{""label"":""Lao"",""value"":""lo"",""displayOrder"":123,""hidden"":false,""readOnly"":false},{""label"":""Latvian"",""value"":""lv"",""displayOrder"":124,""hidden"":false,""readOnly"":true},{""label"":""Latvian - Latvia"",""value"":""lv-lv"",""displayOrder"":125,""hidden"":false,""readOnly"":true},{""label"":""Lithuanian"",""value"":""lt"",""displayOrder"":126,""hidden"":false,""readOnly"":true},{""label"":""Lithuanian - Lithuania"",""value"":""lt-lt"",""displayOrder"":127,""hidden"":false,""readOnly"":true},{""label"":""Macedonian"",""value"":""mk"",""displayOrder"":128,""hidden"":false,""readOnly"":true},{""label"":""Macedonian - Macedonia"",""value"":""mk-mk"",""displayOrder"":129,""hidden"":false,""readOnly"":true},{""label"":""Malagasy"",""value"":""mg"",""displayOrder"":130,""hidden"":false,""readOnly"":false},{""label"":""Malay"",""value"":""ms"",""displayOrder"":131,""hidden"":false,""readOnly"":true},{""label"":""Malayalam"",""value"":""m1"",""displayOrder"":132,""hidden"":false,""readOnly"":true},{""label"":""Malay - Brunei"",""value"":""ms-bn"",""displayOrder"":133,""hidden"":false,""readOnly"":true},{""label"":""Malay - Malaysia"",""value"":""ms-my"",""displayOrder"":134,""hidden"":false,""readOnly"":true},{""label"":""Maltese"",""value"":""mt"",""displayOrder"":135,""hidden"":false,""readOnly"":true},{""label"":""Maltese - Malta"",""value"":""mt-mt"",""displayOrder"":136,""hidden"":false,""readOnly"":true},{""label"":""Marathi"",""value"":""mr"",""displayOrder"":137,""hidden"":false,""readOnly"":true},{""label"":""Mongolian"",""value"":""mn"",""displayOrder"":138,""hidden"":false,""readOnly"":true},{""label"":""Norwegian"",""value"":""no"",""displayOrder"":139,""hidden"":false,""readOnly"":true},{""label"":""Norwegian Bokmal"",""value"":""nb"",""displayOrder"":140,""hidden"":false,""readOnly"":true},{""label"":""Norwegian - Norway"",""value"":""no-no"",""displayOrder"":141,""hidden"":false,""readOnly"":true},{""label"":""Nyanja"",""value"":""ny"",""displayOrder"":142,""hidden"":false,""readOnly"":false},{""label"":""Polish"",""value"":""pl"",""displayOrder"":143,""hidden"":false,""readOnly"":true},{""label"":""Polish - Poland"",""value"":""pl-pl"",""displayOrder"":144,""hidden"":false,""readOnly"":true},{""label"":""Portuguese"",""value"":""pt"",""displayOrder"":145,""hidden"":false,""readOnly"":true},{""label"":""Portuguese - Brazil"",""value"":""pt-br"",""displayOrder"":146,""hidden"":false,""readOnly"":true},{""label"":""Portuguese - Portugal"",""value"":""pt-pt"",""displayOrder"":147,""hidden"":false,""readOnly"":true},{""label"":""Punjabi"",""value"":""pa"",""displayOrder"":148,""hidden"":false,""readOnly"":true},{""label"":""Romanian"",""value"":""ro"",""displayOrder"":149,""hidden"":false,""readOnly"":true},{""label"":""Romanian - Romania"",""value"":""ro-ro"",""displayOrder"":150,""hidden"":false,""readOnly"":true},{""label"":""Russian"",""value"":""ru"",""displayOrder"":151,""hidden"":false,""readOnly"":true},{""label"":""Russian - Russia"",""value"":""ru-ru"",""displayOrder"":152,""hidden"":false,""readOnly"":true},{""label"":""Sanskrit"",""value"":""sa"",""displayOrder"":153,""hidden"":false,""readOnly"":true},{""label"":""Serbian"",""value"":""sr"",""displayOrder"":154,""hidden"":false,""readOnly"":true},{""label"":""Serbian - Bosnia and Herzegovina"",""value"":""sr-ba"",""displayOrder"":155,""hidden"":false,""readOnly"":true},{""label"":""Serbian - Montenegro"",""value"":""sr-me"",""displayOrder"":156,""hidden"":false,""readOnly"":true},{""label"":""Serbian - Serbia"",""value"":""sr-rs"",""displayOrder"":157,""hidden"":false,""readOnly"":true},{""label"":""Serbian - Serbia and Montenegro (Former)"",""value"":""sr-cs"",""displayOrder"":158,""hidden"":false,""readOnly"":true},{""label"":""Slovak"",""value"":""sk"",""displayOrder"":159,""hidden"":false,""readOnly"":true},{""label"":""Slovak - Slovakia"",""value"":""sk-sk"",""displayOrder"":160,""hidden"":false,""readOnly"":true},{""label"":""Slovenian"",""value"":""sl"",""displayOrder"":161,""hidden"":false,""readOnly"":true},{""label"":""Slovenian - Slovenia"",""value"":""sl-si"",""displayOrder"":162,""hidden"":false,""readOnly"":true},{""label"":""Spanish"",""value"":""es"",""displayOrder"":163,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Argentina"",""value"":""es-ar"",""displayOrder"":164,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Bolivia"",""value"":""es-bo"",""displayOrder"":165,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Chile"",""value"":""es-cl"",""displayOrder"":166,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Colombia"",""value"":""es-co"",""displayOrder"":167,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Costa Rica"",""value"":""es-cr"",""displayOrder"":168,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Cuba"",""value"":""es-cu"",""displayOrder"":169,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Dominican Republic"",""value"":""es-do"",""displayOrder"":170,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Ecuador"",""value"":""es-ec"",""displayOrder"":171,""hidden"":false,""readOnly"":true},{""label"":""Spanish - El Salvador"",""value"":""es-sv"",""displayOrder"":172,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Guatemala"",""value"":""es-gt"",""displayOrder"":173,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Honduras"",""value"":""es-hn"",""displayOrder"":174,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Mexico"",""value"":""es-mx"",""displayOrder"":175,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Nicaragua"",""value"":""es-ni"",""displayOrder"":176,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Panama"",""value"":""es-pa"",""displayOrder"":177,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Paraguay"",""value"":""es-py"",""displayOrder"":178,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Peru"",""value"":""es-pe"",""displayOrder"":179,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Puerto Rico"",""value"":""es-pr"",""displayOrder"":180,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Spain"",""value"":""es-es"",""displayOrder"":181,""hidden"":false,""readOnly"":true},{""label"":""Spanish - United States"",""value"":""es-us"",""displayOrder"":182,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Uruguay"",""value"":""es-uy"",""displayOrder"":183,""hidden"":false,""readOnly"":true},{""label"":""Spanish - Venezuela"",""value"":""es-ve"",""displayOrder"":184,""hidden"":false,""readOnly"":true},{""label"":""Swahili"",""value"":""sw"",""displayOrder"":185,""hidden"":false,""readOnly"":true},{""label"":""Swedish"",""value"":""sv"",""displayOrder"":186,""hidden"":false,""readOnly"":true},{""label"":""Swedish - Finland"",""value"":""sv-fi"",""displayOrder"":187,""hidden"":false,""readOnly"":true},{""label"":""Swedish - Sweden"",""value"":""sv-se"",""displayOrder"":188,""hidden"":false,""readOnly"":true},{""label"":""Syriac"",""value"":""sy"",""displayOrder"":189,""hidden"":false,""readOnly"":true},{""label"":""Tagalog"",""value"":""t1"",""displayOrder"":190,""hidden"":false,""readOnly"":true},{""label"":""Tamil"",""value"":""ta"",""displayOrder"":191,""hidden"":false,""readOnly"":true},{""label"":""Tatar"",""value"":""tt"",""displayOrder"":192,""hidden"":false,""readOnly"":true},{""label"":""Telugu"",""value"":""te"",""displayOrder"":193,""hidden"":false,""readOnly"":true},{""label"":""Thai"",""value"":""th"",""displayOrder"":194,""hidden"":false,""readOnly"":true},{""label"":""Thai - Thailand"",""value"":""th-th"",""displayOrder"":195,""hidden"":false,""readOnly"":true},{""label"":""Turkish"",""value"":""tr"",""displayOrder"":196,""hidden"":false,""readOnly"":true},{""label"":""Turkish - Türkiye"",""value"":""tr-tr"",""displayOrder"":197,""hidden"":false,""readOnly"":true},{""label"":""Ukrainian"",""value"":""uk"",""displayOrder"":198,""hidden"":false,""readOnly"":true},{""label"":""Ukrainian - Ukraine"",""value"":""uk-ua"",""displayOrder"":199,""hidden"":false,""readOnly"":true},{""label"":""Urdu"",""value"":""ur"",""displayOrder"":200,""hidden"":false,""readOnly"":true},{""label"":""Vietnamese"",""value"":""vi"",""displayOrder"":201,""hidden"":false,""readOnly"":true},{""label"":""Vietnamese - Vietnam"",""value"":""vi-vn"",""displayOrder"":202,""hidden"":false,""readOnly"":true},{""label"":""Yoruba"",""value"":""yo"",""displayOrder"":203,""hidden"":false,""readOnly"":false}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Latest Disqualified Lead Date,hs_latest_disqualified_lead_date,datetime,The most recent time at which an associated lead currently in a disqualified stage was moved to that stage,contactinformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Latest Open Lead Date,hs_latest_open_lead_date,datetime,The most recent time an associated open lead was moved to a NEW or IN_PROGRESS state,contactinformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Latest Qualified Lead Date,hs_latest_qualified_lead_date,datetime,The most recent time at which an associated lead currently in a qualified stage was moved to that stage,contactinformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Last sequence ended date,hs_latest_sequence_ended_date,datetime,The last sequence ended date.,contactinformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Last sequence enrolled,hs_latest_sequence_enrolled,number,The last sequence enrolled.,contactinformation,FALSE,,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot, +Last sequence enrolled date,hs_latest_sequence_enrolled_date,datetime,The last sequence enrolled date.,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Membership last private content access date,hs_membership_last_private_content_access_date,datetime,The last date a contact accessed private content,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Merged Contact IDs,hs_merged_object_ids,enumeration,The list of Contact record IDs that have been merged into this Contact. This value is set automatically by HubSpot.,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Record ID,hs_object_id,number,The unique ID for this record. This value is set automatically by HubSpot.,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Record source detail 1,hs_object_source_detail_1,string,First level of detail on how this record was created.,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Record source detail 2,hs_object_source_detail_2,string,Second level of detail on how this record was created.,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Record source detail 3,hs_object_source_detail_3,string,Third level of detail on how this record was created.,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Record source,hs_object_source_label,enumeration,How this record was created.,contactinformation,FALSE,,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot, +Persona,hs_persona,enumeration,A contact's persona,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Registration Method,hs_registration_method,string,The method used for registration,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Employment Role,hs_role,enumeration,Job role,contactinformation,TRUE,"[{""label"":""Accounting"",""value"":""accounting"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""Administrative"",""value"":""administrative"",""displayOrder"":1,""hidden"":false,""readOnly"":false},{""label"":""Business Development"",""value"":""business_development"",""displayOrder"":2,""hidden"":false,""readOnly"":false},{""label"":""Communications"",""value"":""communications"",""displayOrder"":3,""hidden"":false,""readOnly"":false},{""label"":""Consulting"",""value"":""consulting"",""displayOrder"":4,""hidden"":false,""readOnly"":false},{""label"":""Customer Service"",""value"":""customer_service"",""displayOrder"":5,""hidden"":false,""readOnly"":false},{""label"":""Design"",""value"":""design"",""displayOrder"":6,""hidden"":false,""readOnly"":false},{""label"":""Education"",""value"":""education"",""displayOrder"":7,""hidden"":false,""readOnly"":false},{""label"":""Engineering"",""value"":""engineering"",""displayOrder"":8,""hidden"":false,""readOnly"":false},{""label"":""Entrepreneurship"",""value"":""entrepreneurship"",""displayOrder"":9,""hidden"":false,""readOnly"":false},{""label"":""Finance"",""value"":""finance"",""displayOrder"":10,""hidden"":false,""readOnly"":false},{""label"":""Health Professional"",""value"":""health_professional"",""displayOrder"":11,""hidden"":false,""readOnly"":false},{""label"":""Human Resources"",""value"":""human_resources"",""displayOrder"":12,""hidden"":false,""readOnly"":false},{""label"":""Information Technology"",""value"":""information_technology"",""displayOrder"":13,""hidden"":false,""readOnly"":false},{""label"":""Legal"",""value"":""legal"",""displayOrder"":14,""hidden"":false,""readOnly"":false},{""label"":""Marketing"",""value"":""marketing"",""displayOrder"":15,""hidden"":false,""readOnly"":false},{""label"":""Operations"",""value"":""operations"",""displayOrder"":16,""hidden"":false,""readOnly"":false},{""label"":""Product"",""value"":""product"",""displayOrder"":17,""hidden"":false,""readOnly"":false},{""label"":""Project Management"",""value"":""project_management"",""displayOrder"":18,""hidden"":false,""readOnly"":false},{""label"":""Public Relations"",""value"":""public_relations"",""displayOrder"":19,""hidden"":false,""readOnly"":false},{""label"":""Quality Assurance"",""value"":""quality_assurance"",""displayOrder"":20,""hidden"":false,""readOnly"":false},{""label"":""Real Estate"",""value"":""real_estate"",""displayOrder"":21,""hidden"":false,""readOnly"":false},{""label"":""Recruiting"",""value"":""recruiting"",""displayOrder"":22,""hidden"":false,""readOnly"":false},{""label"":""Research"",""value"":""research"",""displayOrder"":23,""hidden"":false,""readOnly"":false},{""label"":""Sales"",""value"":""sales"",""displayOrder"":24,""hidden"":false,""readOnly"":false},{""label"":""Support"",""value"":""support"",""displayOrder"":25,""hidden"":false,""readOnly"":false},{""label"":""Retired"",""value"":""retired"",""displayOrder"":26,""hidden"":false,""readOnly"":false}]",FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Date of first engagement,hs_sa_first_engagement_date,datetime,The date the current contact owner first engaged with the contact.,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Description of first engagement,hs_sa_first_engagement_descr,enumeration,A description of the current contact owner's first engagement with the contact.,contactinformation,FALSE,"[{""label"":"""",""value"":""EMPTY_DESCRIPTION"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Bounced"",""value"":""BOUNCED"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Busy"",""value"":""BUSY"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Calling CRM user"",""value"":""CALLING_CRM_USER"",""displayOrder"":3,""doubleData"":0.0,""hidden"":true,""description"":"""",""readOnly"":false},{""label"":""Canceled"",""value"":""CANCELED"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Completed"",""value"":""COMPLETED"",""displayOrder"":5,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Connecting"",""value"":""CONNECTING"",""displayOrder"":6,""doubleData"":0.0,""hidden"":true,""description"":"""",""readOnly"":false},{""label"":""Deferred"",""value"":""DEFERRED"",""displayOrder"":7,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Facebook Messenger"",""value"":""FB_MESSENGER"",""displayOrder"":8,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Failed"",""value"":""FAILED"",""displayOrder"":9,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""In progress"",""value"":""IN_PROGRESS"",""displayOrder"":10,""doubleData"":0.0,""hidden"":true,""description"":"""",""readOnly"":false},{""label"":""Live chat"",""value"":""LIVE_CHAT"",""displayOrder"":11,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Logged call"",""value"":""LOGGED_CALL"",""displayOrder"":12,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""No answer"",""value"":""NO_ANSWER"",""displayOrder"":13,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""No show"",""value"":""NO_SHOW"",""displayOrder"":14,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Not started"",""value"":""NOT_STARTED"",""displayOrder"":15,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Queued"",""value"":""QUEUED"",""displayOrder"":16,""doubleData"":0.0,""hidden"":true,""description"":"""",""readOnly"":false},{""label"":""Rescheduled"",""value"":""RESCHEDULED"",""displayOrder"":17,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Ringing"",""value"":""RINGING"",""displayOrder"":18,""doubleData"":0.0,""hidden"":true,""description"":"""",""readOnly"":false},{""label"":""Scheduled"",""value"":""SCHEDULED"",""displayOrder"":19,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Sending"",""value"":""SENDING"",""displayOrder"":20,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Sent"",""value"":""SENT"",""displayOrder"":21,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Waiting"",""value"":""WAITING"",""displayOrder"":22,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Missed"",""value"":""MISSED"",""displayOrder"":23,""hidden"":false,""readOnly"":false},{""label"":""On Hold"",""value"":""HOLD"",""displayOrder"":24,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Type of first engagement,hs_sa_first_engagement_object_type,enumeration,The object type of the current contact owner's first engagement with the contact.,contactinformation,FALSE,"[{""label"":""Call"",""value"":""CALL"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Conversation session"",""value"":""CONVERSATION_SESSION"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Email"",""value"":""EMAIL"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Meeting"",""value"":""MEETING_EVENT"",""displayOrder"":3,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Task"",""value"":""TASK"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Employment Seniority,hs_seniority,enumeration,Job Seniority,contactinformation,TRUE,"[{""label"":""VP"",""value"":""vp"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""Director"",""value"":""director"",""displayOrder"":1,""hidden"":false,""readOnly"":false},{""label"":""Entry"",""value"":""entry"",""displayOrder"":2,""hidden"":false,""readOnly"":false},{""label"":""Executive"",""value"":""executive"",""displayOrder"":3,""hidden"":false,""readOnly"":false},{""label"":""Manager"",""value"":""manager"",""displayOrder"":4,""hidden"":false,""readOnly"":false},{""label"":""Owner"",""value"":""owner"",""displayOrder"":5,""hidden"":false,""readOnly"":false},{""label"":""Partner"",""value"":""partner"",""displayOrder"":6,""hidden"":false,""readOnly"":false},{""label"":""Senior"",""value"":""senior"",""displayOrder"":7,""hidden"":false,""readOnly"":false},{""label"":""Employee"",""value"":""employee"",""displayOrder"":8,""hidden"":false,""readOnly"":false}]",FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of sequences enrolled,hs_sequences_enrolled_count,number,The number of sequences enrolled.,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +State/Region Code,hs_state_code,string,The contact's state or region code.,contactinformation,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Employment Sub Role,hs_sub_role,enumeration,Job sub role,contactinformation,TRUE,"[{""label"":""Account Executive"",""value"":""account_executive"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""Account Manager"",""value"":""account_manager"",""displayOrder"":1,""hidden"":false,""readOnly"":false},{""label"":""Accountant"",""value"":""accountant"",""displayOrder"":2,""hidden"":false,""readOnly"":false},{""label"":""Accounting Manager"",""value"":""accounting_manager"",""displayOrder"":3,""hidden"":false,""readOnly"":false},{""label"":""Administrative Assistant"",""value"":""administrative_assistant"",""displayOrder"":4,""hidden"":false,""readOnly"":false},{""label"":""Appraisal"",""value"":""appraisal"",""displayOrder"":5,""hidden"":false,""readOnly"":false},{""label"":""Architect IT"",""value"":""architect_it"",""displayOrder"":6,""hidden"":false,""readOnly"":false},{""label"":""Assistant"",""value"":""assistant"",""displayOrder"":7,""hidden"":false,""readOnly"":false},{""label"":""Attorney"",""value"":""attorney"",""displayOrder"":8,""hidden"":false,""readOnly"":false},{""label"":""Auditor"",""value"":""auditor"",""displayOrder"":9,""hidden"":false,""readOnly"":false},{""label"":""Brand Marketing"",""value"":""brand_marketing"",""displayOrder"":10,""hidden"":false,""readOnly"":false},{""label"":""Business Analyst"",""value"":""business_analyst"",""displayOrder"":11,""hidden"":false,""readOnly"":false},{""label"":""Business Consultant"",""value"":""business_consultant"",""displayOrder"":12,""hidden"":false,""readOnly"":false},{""label"":""Business Manager"",""value"":""business_manager"",""displayOrder"":13,""hidden"":false,""readOnly"":false},{""label"":""Chief Financial Officer"",""value"":""chief_financial_officer"",""displayOrder"":14,""hidden"":false,""readOnly"":false},{""label"":""Communications Manager"",""value"":""communications_manager"",""displayOrder"":15,""hidden"":false,""readOnly"":false},{""label"":""Community"",""value"":""community"",""displayOrder"":16,""hidden"":false,""readOnly"":false},{""label"":""Content Marketing"",""value"":""content_marketing"",""displayOrder"":17,""hidden"":false,""readOnly"":false},{""label"":""Contracts"",""value"":""contracts"",""displayOrder"":18,""hidden"":false,""readOnly"":false},{""label"":""Creative"",""value"":""creative"",""displayOrder"":19,""hidden"":false,""readOnly"":false},{""label"":""Customer Service Specialist"",""value"":""customer_service_specialist"",""displayOrder"":20,""hidden"":false,""readOnly"":false},{""label"":""Customer Success"",""value"":""customer_success"",""displayOrder"":21,""hidden"":false,""readOnly"":false},{""label"":""Data IT"",""value"":""data_it"",""displayOrder"":22,""hidden"":false,""readOnly"":false},{""label"":""Data Science Engineer"",""value"":""data_science_engineer"",""displayOrder"":23,""hidden"":false,""readOnly"":false},{""label"":""Database Administrator"",""value"":""database_administrator"",""displayOrder"":24,""hidden"":false,""readOnly"":false},{""label"":""Design Engineer"",""value"":""design_engineer"",""displayOrder"":25,""hidden"":false,""readOnly"":false},{""label"":""Development Specialist"",""value"":""development_specialist"",""displayOrder"":26,""hidden"":false,""readOnly"":false},{""label"":""Devops Engineer"",""value"":""devops_engineer"",""displayOrder"":27,""hidden"":false,""readOnly"":false},{""label"":""Digital Marketing"",""value"":""digital_marketing"",""displayOrder"":28,""hidden"":false,""readOnly"":false},{""label"":""Director of Development"",""value"":""director_of_development"",""displayOrder"":29,""hidden"":false,""readOnly"":false},{""label"":""Editorial"",""value"":""editorial"",""displayOrder"":30,""hidden"":false,""readOnly"":false},{""label"":""Electrical Engineer"",""value"":""electrical_engineer"",""displayOrder"":31,""hidden"":false,""readOnly"":false},{""label"":""Engineering Manager"",""value"":""engineering_manager"",""displayOrder"":32,""hidden"":false,""readOnly"":false},{""label"":""Events"",""value"":""events"",""displayOrder"":33,""hidden"":false,""readOnly"":false},{""label"":""Executive Assistant"",""value"":""executive_assistant"",""displayOrder"":34,""hidden"":false,""readOnly"":false},{""label"":""Facilities"",""value"":""facilities"",""displayOrder"":35,""hidden"":false,""readOnly"":false},{""label"":""Fashion Design"",""value"":""fashion_design"",""displayOrder"":36,""hidden"":false,""readOnly"":false},{""label"":""Field Marketing"",""value"":""field_marketing"",""displayOrder"":37,""hidden"":false,""readOnly"":false},{""label"":""Financial Analyst"",""value"":""financial_analyst"",""displayOrder"":38,""hidden"":false,""readOnly"":false},{""label"":""Financial Controller"",""value"":""financial_controller"",""displayOrder"":39,""hidden"":false,""readOnly"":false},{""label"":""Fitness"",""value"":""fitness"",""displayOrder"":40,""hidden"":false,""readOnly"":false},{""label"":""Founder"",""value"":""founder"",""displayOrder"":41,""hidden"":false,""readOnly"":false},{""label"":""General Counsel"",""value"":""general_counsel"",""displayOrder"":42,""hidden"":false,""readOnly"":false},{""label"":""General Manager"",""value"":""general_manager"",""displayOrder"":43,""hidden"":false,""readOnly"":false},{""label"":""General Partner"",""value"":""general_partner"",""displayOrder"":44,""hidden"":false,""readOnly"":false},{""label"":""Graphic Design"",""value"":""graphic_design"",""displayOrder"":45,""hidden"":false,""readOnly"":false},{""label"":""Human Resources Specialist"",""value"":""human_resources_specialist"",""displayOrder"":46,""hidden"":false,""readOnly"":false},{""label"":""Information Technology Specialist"",""value"":""information_technology_specialist"",""displayOrder"":47,""hidden"":false,""readOnly"":false},{""label"":""Investment"",""value"":""investment"",""displayOrder"":48,""hidden"":false,""readOnly"":false},{""label"":""Investment Banker"",""value"":""investment_banker"",""displayOrder"":49,""hidden"":false,""readOnly"":false},{""label"":""Journalist"",""value"":""journalist"",""displayOrder"":50,""hidden"":false,""readOnly"":false},{""label"":""Key Account Manager"",""value"":""key_account_manager"",""displayOrder"":51,""hidden"":false,""readOnly"":false},{""label"":""Law Enforcement"",""value"":""law_enforcement"",""displayOrder"":52,""hidden"":false,""readOnly"":false},{""label"":""Lawyer"",""value"":""lawyer"",""displayOrder"":53,""hidden"":false,""readOnly"":false},{""label"":""Logistics Manager"",""value"":""logistics_manager"",""displayOrder"":54,""hidden"":false,""readOnly"":false},{""label"":""Management"",""value"":""management"",""displayOrder"":55,""hidden"":false,""readOnly"":false},{""label"":""Marketing Specialist"",""value"":""marketing_specialist"",""displayOrder"":56,""hidden"":false,""readOnly"":false},{""label"":""Mechanical Engineer"",""value"":""mechanical_engineer"",""displayOrder"":57,""hidden"":false,""readOnly"":false},{""label"":""Medical Doctor"",""value"":""medical_doctor"",""displayOrder"":58,""hidden"":false,""readOnly"":false},{""label"":""Network Engineer"",""value"":""network_engineer"",""displayOrder"":59,""hidden"":false,""readOnly"":false},{""label"":""Nurse"",""value"":""nurse"",""displayOrder"":60,""hidden"":false,""readOnly"":false},{""label"":""Office Management"",""value"":""office_management"",""displayOrder"":61,""hidden"":false,""readOnly"":false},{""label"":""Office Manager"",""value"":""office_manager"",""displayOrder"":62,""hidden"":false,""readOnly"":false},{""label"":""Operational Specialist"",""value"":""operational_specialist"",""displayOrder"":63,""hidden"":false,""readOnly"":false},{""label"":""Owner"",""value"":""owner"",""displayOrder"":64,""hidden"":false,""readOnly"":false},{""label"":""Paralegal"",""value"":""paralegal"",""displayOrder"":65,""hidden"":false,""readOnly"":false},{""label"":""Principal"",""value"":""principal"",""displayOrder"":66,""hidden"":false,""readOnly"":false},{""label"":""Product Design"",""value"":""product_design"",""displayOrder"":67,""hidden"":false,""readOnly"":false},{""label"":""Product Manager"",""value"":""product_manager"",""displayOrder"":68,""hidden"":false,""readOnly"":false},{""label"":""Product Marketing"",""value"":""product_marketing"",""displayOrder"":69,""hidden"":false,""readOnly"":false},{""label"":""Production Manager"",""value"":""production_manager"",""displayOrder"":70,""hidden"":false,""readOnly"":false},{""label"":""Professor"",""value"":""professor"",""displayOrder"":71,""hidden"":false,""readOnly"":false},{""label"":""Program Coordinator"",""value"":""program_coordinator"",""displayOrder"":72,""hidden"":false,""readOnly"":false},{""label"":""Program Manager"",""value"":""program_manager"",""displayOrder"":73,""hidden"":false,""readOnly"":false},{""label"":""Project Engineer"",""value"":""project_engineer"",""displayOrder"":74,""hidden"":false,""readOnly"":false},{""label"":""Project Manager"",""value"":""project_manager"",""displayOrder"":75,""hidden"":false,""readOnly"":false},{""label"":""Property Manager"",""value"":""property_manager"",""displayOrder"":76,""hidden"":false,""readOnly"":false},{""label"":""QA engineer"",""value"":""qa_engineer"",""displayOrder"":77,""hidden"":false,""readOnly"":false},{""label"":""QA IT"",""value"":""qa_it"",""displayOrder"":78,""hidden"":false,""readOnly"":false},{""label"":""Quality Assurance Manager"",""value"":""quality_assurance_manager"",""displayOrder"":79,""hidden"":false,""readOnly"":false},{""label"":""Quality Assurance Specialist"",""value"":""quality_assurance_specialist"",""displayOrder"":80,""hidden"":false,""readOnly"":false},{""label"":""Realtor"",""value"":""realtor"",""displayOrder"":81,""hidden"":false,""readOnly"":false},{""label"":""Recruiter"",""value"":""recruiter"",""displayOrder"":82,""hidden"":false,""readOnly"":false},{""label"":""Relationship Manager"",""value"":""relationship_manager"",""displayOrder"":83,""hidden"":false,""readOnly"":false},{""label"":""Research Analyst"",""value"":""research_analyst"",""displayOrder"":84,""hidden"":false,""readOnly"":false},{""label"":""Risk Compliance"",""value"":""risk_compliance"",""displayOrder"":85,""hidden"":false,""readOnly"":false},{""label"":""Sales Executive"",""value"":""sales_executive"",""displayOrder"":86,""hidden"":false,""readOnly"":false},{""label"":""Sales Operations"",""value"":""sales_operations"",""displayOrder"":87,""hidden"":false,""readOnly"":false},{""label"":""Sales Specialist"",""value"":""sales_specialist"",""displayOrder"":88,""hidden"":false,""readOnly"":false},{""label"":""Salesperson"",""value"":""salesperson"",""displayOrder"":89,""hidden"":false,""readOnly"":false},{""label"":""Secretary"",""value"":""secretary"",""displayOrder"":90,""hidden"":false,""readOnly"":false},{""label"":""Social Marketing"",""value"":""social_marketing"",""displayOrder"":91,""hidden"":false,""readOnly"":false},{""label"":""Software Engineer"",""value"":""software_engineer"",""displayOrder"":92,""hidden"":false,""readOnly"":false},{""label"":""Strategy"",""value"":""strategy"",""displayOrder"":93,""hidden"":false,""readOnly"":false},{""label"":""Student"",""value"":""student"",""displayOrder"":94,""hidden"":false,""readOnly"":false},{""label"":""Support"",""value"":""support"",""displayOrder"":95,""hidden"":false,""readOnly"":false},{""label"":""Support Specialist"",""value"":""support_specialist"",""displayOrder"":96,""hidden"":false,""readOnly"":false},{""label"":""System Administrator"",""value"":""system_administrator"",""displayOrder"":97,""hidden"":false,""readOnly"":false},{""label"":""System Analyst"",""value"":""system_analyst"",""displayOrder"":98,""hidden"":false,""readOnly"":false},{""label"":""Systems Engineer"",""value"":""systems_engineer"",""displayOrder"":99,""hidden"":false,""readOnly"":false},{""label"":""Talent"",""value"":""talent"",""displayOrder"":100,""hidden"":false,""readOnly"":false},{""label"":""Tax Audit"",""value"":""tax_audit"",""displayOrder"":101,""hidden"":false,""readOnly"":false},{""label"":""Teacher"",""value"":""teacher"",""displayOrder"":102,""hidden"":false,""readOnly"":false},{""label"":""Technical Manager"",""value"":""technical_manager"",""displayOrder"":103,""hidden"":false,""readOnly"":false},{""label"":""Technical Support Specialist"",""value"":""technical_support_specialist"",""displayOrder"":104,""hidden"":false,""readOnly"":false},{""label"":""Therapist"",""value"":""therapist"",""displayOrder"":105,""hidden"":false,""readOnly"":false},{""label"":""Training"",""value"":""training"",""displayOrder"":106,""hidden"":false,""readOnly"":false},{""label"":""Video"",""value"":""video"",""displayOrder"":107,""hidden"":false,""readOnly"":false},{""label"":""Web Developer"",""value"":""web_developer"",""displayOrder"":108,""hidden"":false,""readOnly"":false},{""label"":""Writer"",""value"":""writer"",""displayOrder"":109,""hidden"":false,""readOnly"":false},{""label"":""Chief Executive Officer"",""value"":""chief_executive_officer"",""displayOrder"":110,""hidden"":false,""readOnly"":false},{""label"":""Chief Operating Officer"",""value"":""chief_operating_officer"",""displayOrder"":111,""hidden"":false,""readOnly"":false},{""label"":""Chief Marketing Officer"",""value"":""chief_marketing_officer"",""displayOrder"":112,""hidden"":false,""readOnly"":false},{""label"":""Chief Technology Officer"",""value"":""chief_technology_officer"",""displayOrder"":113,""hidden"":false,""readOnly"":false},{""label"":""Chief Information Officer"",""value"":""chief_information_officer"",""displayOrder"":114,""hidden"":false,""readOnly"":false},{""label"":""Chief Human Resources officer"",""value"":""chief_human_resources_officer"",""displayOrder"":115,""hidden"":false,""readOnly"":false},{""label"":""Chief Compliance Officer"",""value"":""chief_compliance_officer"",""displayOrder"":116,""hidden"":false,""readOnly"":false},{""label"":""Chief Risk Officer"",""value"":""chief_risk_officer"",""displayOrder"":117,""hidden"":false,""readOnly"":false},{""label"":""Chief Data Officer"",""value"":""chief_data_officer"",""displayOrder"":118,""hidden"":false,""readOnly"":false},{""label"":""Chief Product Officer"",""value"":""chief_product_officer"",""displayOrder"":119,""hidden"":false,""readOnly"":false},{""label"":""Chief Revenue Officer"",""value"":""chief_revenue_officer"",""displayOrder"":120,""hidden"":false,""readOnly"":false},{""label"":""Chief Sustainability Officer"",""value"":""chief_sustainability_officer"",""displayOrder"":121,""hidden"":false,""readOnly"":false},{""label"":""Chief Legal Officer"",""value"":""chief_legal_officer"",""displayOrder"":122,""hidden"":false,""readOnly"":false},{""label"":""Chief Security Officer"",""value"":""chief_security_officer"",""displayOrder"":123,""hidden"":false,""readOnly"":false},{""label"":""Chief Experience Officer"",""value"":""chief_experience_officer"",""displayOrder"":124,""hidden"":false,""readOnly"":false},{""label"":""Chief Innovation Officer"",""value"":""chief_innovation_officer"",""displayOrder"":125,""hidden"":false,""readOnly"":false},{""label"":""Retail"",""value"":""retail"",""displayOrder"":126,""hidden"":false,""readOnly"":false},{""label"":""Retired"",""value"":""retired"",""displayOrder"":127,""hidden"":false,""readOnly"":false}]",FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Lead response time,hs_time_to_first_engagement,number,"The time it took the current owner to perform a qualifying action on the contact. Qualifying actions include sending an email, calling, using chat, recording a meeting outcome, or marking a task as in progress or completed.",contactinformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Time Zone,hs_timezone,enumeration,The contact’s time zone. This can be set automatically by HubSpot based on other contact properties. It can also be set manually for each contact.,contactinformation,TRUE,"[{""label"":""UTC -11:00 Pacific Midway"",""value"":""pacific_slash_midway"",""displayOrder"":1,""hidden"":false,""readOnly"":true},{""label"":""UTC -11:00 Pacific Niue"",""value"":""pacific_slash_niue"",""displayOrder"":2,""hidden"":false,""readOnly"":true},{""label"":""UTC -11:00 Pacific Pago Pago"",""value"":""pacific_slash_pago_pago"",""displayOrder"":3,""hidden"":false,""readOnly"":true},{""label"":""UTC -11:00 Pacific Samoa"",""value"":""pacific_slash_samoa"",""displayOrder"":4,""hidden"":false,""readOnly"":true},{""label"":""UTC -11:00 US Samoa"",""value"":""us_slash_samoa"",""displayOrder"":5,""hidden"":false,""readOnly"":true},{""label"":""UTC -10:00 Pacific Honolulu"",""value"":""pacific_slash_honolulu"",""displayOrder"":6,""hidden"":false,""readOnly"":true},{""label"":""UTC -10:00 Pacific Johnston"",""value"":""pacific_slash_johnston"",""displayOrder"":7,""hidden"":false,""readOnly"":true},{""label"":""UTC -10:00 Pacific Rarotonga"",""value"":""pacific_slash_rarotonga"",""displayOrder"":8,""hidden"":false,""readOnly"":true},{""label"":""UTC -10:00 Pacific Tahiti"",""value"":""pacific_slash_tahiti"",""displayOrder"":9,""hidden"":false,""readOnly"":true},{""label"":""UTC -10:00 US Hawaii"",""value"":""us_slash_hawaii"",""displayOrder"":10,""hidden"":false,""readOnly"":true},{""label"":""UTC -09:30 Pacific Marquesas"",""value"":""pacific_slash_marquesas"",""displayOrder"":11,""hidden"":false,""readOnly"":true},{""label"":""UTC -09:00 America Adak"",""value"":""america_slash_adak"",""displayOrder"":12,""hidden"":false,""readOnly"":true},{""label"":""UTC -09:00 America Atka"",""value"":""america_slash_atka"",""displayOrder"":13,""hidden"":false,""readOnly"":true},{""label"":""UTC -09:00 Pacific Gambier"",""value"":""pacific_slash_gambier"",""displayOrder"":14,""hidden"":false,""readOnly"":true},{""label"":""UTC -09:00 US Aleutian"",""value"":""us_slash_aleutian"",""displayOrder"":15,""hidden"":false,""readOnly"":true},{""label"":""UTC -08:00 America Anchorage"",""value"":""america_slash_anchorage"",""displayOrder"":16,""hidden"":false,""readOnly"":true},{""label"":""UTC -08:00 America Juneau"",""value"":""america_slash_juneau"",""displayOrder"":17,""hidden"":false,""readOnly"":true},{""label"":""UTC -08:00 America Metlakatla"",""value"":""america_slash_metlakatla"",""displayOrder"":18,""hidden"":false,""readOnly"":true},{""label"":""UTC -08:00 America Nome"",""value"":""america_slash_nome"",""displayOrder"":19,""hidden"":false,""readOnly"":true},{""label"":""UTC -08:00 America Sitka"",""value"":""america_slash_sitka"",""displayOrder"":20,""hidden"":false,""readOnly"":true},{""label"":""UTC -08:00 America Yakutat"",""value"":""america_slash_yakutat"",""displayOrder"":21,""hidden"":false,""readOnly"":true},{""label"":""UTC -08:00 Pacific Pitcairn"",""value"":""pacific_slash_pitcairn"",""displayOrder"":22,""hidden"":false,""readOnly"":true},{""label"":""UTC -08:00 US Alaska"",""value"":""us_slash_alaska"",""displayOrder"":23,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Creston"",""value"":""america_slash_creston"",""displayOrder"":24,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Dawson"",""value"":""america_slash_dawson"",""displayOrder"":25,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Dawson Creek"",""value"":""america_slash_dawson_creek"",""displayOrder"":26,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Ensenada"",""value"":""america_slash_ensenada"",""displayOrder"":27,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Fort Nelson"",""value"":""america_slash_fort_nelson"",""displayOrder"":28,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Hermosillo"",""value"":""america_slash_hermosillo"",""displayOrder"":29,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Los Angeles"",""value"":""america_slash_los_angeles"",""displayOrder"":30,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Phoenix"",""value"":""america_slash_phoenix"",""displayOrder"":31,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Santa Isabel"",""value"":""america_slash_santa_isabel"",""displayOrder"":32,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Tijuana"",""value"":""america_slash_tijuana"",""displayOrder"":33,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Vancouver"",""value"":""america_slash_vancouver"",""displayOrder"":34,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 America Whitehorse"",""value"":""america_slash_whitehorse"",""displayOrder"":35,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 Canada Pacific"",""value"":""canada_slash_pacific"",""displayOrder"":36,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 Canada Yukon"",""value"":""canada_slash_yukon"",""displayOrder"":37,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 Mexico BajaNorte"",""value"":""mexico_slash_bajanorte"",""displayOrder"":38,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 US Arizona"",""value"":""us_slash_arizona"",""displayOrder"":39,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 US Pacific"",""value"":""us_slash_pacific"",""displayOrder"":40,""hidden"":false,""readOnly"":true},{""label"":""UTC -07:00 US Pacific-New"",""value"":""us_slash_pacific_hyphen_new"",""displayOrder"":41,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Belize"",""value"":""america_slash_belize"",""displayOrder"":42,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Boise"",""value"":""america_slash_boise"",""displayOrder"":43,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Cambridge Bay"",""value"":""america_slash_cambridge_bay"",""displayOrder"":44,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Chihuahua"",""value"":""america_slash_chihuahua"",""displayOrder"":45,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Costa Rica"",""value"":""america_slash_costa_rica"",""displayOrder"":46,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Denver"",""value"":""america_slash_denver"",""displayOrder"":47,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Edmonton"",""value"":""america_slash_edmonton"",""displayOrder"":48,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America El Salvador"",""value"":""america_slash_el_salvador"",""displayOrder"":49,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Guatemala"",""value"":""america_slash_guatemala"",""displayOrder"":50,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Inuvik"",""value"":""america_slash_inuvik"",""displayOrder"":51,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Managua"",""value"":""america_slash_managua"",""displayOrder"":52,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Mazatlan"",""value"":""america_slash_mazatlan"",""displayOrder"":53,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Navajo"",""value"":""navajo"",""displayOrder"":54,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Ojinaga"",""value"":""america_slash_ojinaga"",""displayOrder"":55,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Regina"",""value"":""america_slash_regina"",""displayOrder"":56,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Shiprock"",""value"":""america_slash_shiprock"",""displayOrder"":57,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Swift Current"",""value"":""america_slash_swift_current"",""displayOrder"":58,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Tegucigalpa"",""value"":""america_slash_tegucigalpa"",""displayOrder"":59,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 America Yellowknife"",""value"":""america_slash_yellowknife"",""displayOrder"":60,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 Canada Mountain"",""value"":""canada_slash_mountain"",""displayOrder"":61,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 Canada Saskatchewan"",""value"":""canada_slash_saskatchewan"",""displayOrder"":62,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 Chile EasterIsland"",""value"":""chile_slash_easterisland"",""displayOrder"":63,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 Mexico BajaSur"",""value"":""mexico_slash_bajasur"",""displayOrder"":64,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 Pacific Easter"",""value"":""pacific_slash_easter"",""displayOrder"":65,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 Pacific Galapagos"",""value"":""pacific_slash_galapagos"",""displayOrder"":66,""hidden"":false,""readOnly"":true},{""label"":""UTC -06:00 US Mountain"",""value"":""us_slash_mountain"",""displayOrder"":67,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Atikokan"",""value"":""america_slash_atikokan"",""displayOrder"":68,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Bahia Banderas"",""value"":""america_slash_bahia_banderas"",""displayOrder"":69,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Bogota"",""value"":""america_slash_bogota"",""displayOrder"":70,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Cancun"",""value"":""america_slash_cancun"",""displayOrder"":71,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Cayman"",""value"":""america_slash_cayman"",""displayOrder"":72,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Chicago"",""value"":""america_slash_chicago"",""displayOrder"":73,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Coral Harbour"",""value"":""america_slash_coral_harbour"",""displayOrder"":74,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Eirunepe"",""value"":""america_slash_eirunepe"",""displayOrder"":75,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Guayaquil"",""value"":""america_slash_guayaquil"",""displayOrder"":76,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Indiana (Knox)"",""value"":""america_slash_indiana_slash_knox"",""displayOrder"":77,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Indiana (Tell City)"",""value"":""america_slash_indiana_slash_tell_city"",""displayOrder"":78,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Jamaica"",""value"":""america_slash_jamaica"",""displayOrder"":79,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Knox IN"",""value"":""america_slash_knox_in"",""displayOrder"":80,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Lima"",""value"":""america_slash_lima"",""displayOrder"":81,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Matamoros"",""value"":""america_slash_matamoros"",""displayOrder"":82,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Menominee"",""value"":""america_slash_menominee"",""displayOrder"":83,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Merida"",""value"":""america_slash_merida"",""displayOrder"":84,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Mexico City"",""value"":""america_slash_mexico_city"",""displayOrder"":85,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Monterrey"",""value"":""america_slash_monterrey"",""displayOrder"":86,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America North Dakota (Beulah)"",""value"":""america_slash_north_dakota_slash_beulah"",""displayOrder"":87,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America North Dakota (Center)"",""value"":""america_slash_north_dakota_slash_center"",""displayOrder"":88,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America North Dakota (New Salem)"",""value"":""america_slash_north_dakota_slash_new_salem"",""displayOrder"":89,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Panama"",""value"":""america_slash_panama"",""displayOrder"":90,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Porto Acre"",""value"":""america_slash_porto_acre"",""displayOrder"":91,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Rainy River"",""value"":""america_slash_rainy_river"",""displayOrder"":92,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Rankin Inlet"",""value"":""america_slash_rankin_inlet"",""displayOrder"":93,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Resolute"",""value"":""america_slash_resolute"",""displayOrder"":94,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Rio Branco"",""value"":""america_slash_rio_branco"",""displayOrder"":95,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 America Winnipeg"",""value"":""america_slash_winnipeg"",""displayOrder"":96,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 Brazil Acre"",""value"":""brazil_slash_acre"",""displayOrder"":97,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 Canada Central"",""value"":""canada_slash_central"",""displayOrder"":98,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 Mexico General"",""value"":""mexico_slash_general"",""displayOrder"":99,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 US Central"",""value"":""us_slash_central"",""displayOrder"":100,""hidden"":false,""readOnly"":true},{""label"":""UTC -05:00 US Indiana-Starke"",""value"":""us_slash_indiana_hyphen_starke"",""displayOrder"":101,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Anguilla"",""value"":""america_slash_anguilla"",""displayOrder"":102,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Antigua"",""value"":""america_slash_antigua"",""displayOrder"":103,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Aruba"",""value"":""america_slash_aruba"",""displayOrder"":104,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Asuncion"",""value"":""america_slash_asuncion"",""displayOrder"":105,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Barbados"",""value"":""america_slash_barbados"",""displayOrder"":106,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Blanc-Sablon"",""value"":""america_slash_blanc_hyphen_sablon"",""displayOrder"":107,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Boa Vista"",""value"":""america_slash_boa_vista"",""displayOrder"":108,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Campo Grande"",""value"":""america_slash_campo_grande"",""displayOrder"":109,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Caracas"",""value"":""america_slash_caracas"",""displayOrder"":110,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Cuba"",""value"":""cuba"",""displayOrder"":111,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Cuiaba"",""value"":""america_slash_cuiaba"",""displayOrder"":112,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Curacao"",""value"":""america_slash_curacao"",""displayOrder"":113,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Detroit"",""value"":""america_slash_detroit"",""displayOrder"":114,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Dominica"",""value"":""america_slash_dominica"",""displayOrder"":115,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Fort Wayne"",""value"":""america_slash_fort_wayne"",""displayOrder"":116,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Grand Turk"",""value"":""america_slash_grand_turk"",""displayOrder"":117,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Grenada"",""value"":""america_slash_grenada"",""displayOrder"":118,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Guadeloupe"",""value"":""america_slash_guadeloupe"",""displayOrder"":119,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Guyana"",""value"":""america_slash_guyana"",""displayOrder"":120,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Havana"",""value"":""america_slash_havana"",""displayOrder"":121,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Indiana (Indianapolis)"",""value"":""america_slash_indiana_slash_indianapolis"",""displayOrder"":122,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Indiana (Marengo)"",""value"":""america_slash_indiana_slash_marengo"",""displayOrder"":123,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Indiana (Petersburg)"",""value"":""america_slash_indiana_slash_petersburg"",""displayOrder"":124,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Indiana (Vevay)"",""value"":""america_slash_indiana_slash_vevay"",""displayOrder"":125,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Indiana (Vincennes)"",""value"":""america_slash_indiana_slash_vincennes"",""displayOrder"":126,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Indiana (Winamac)"",""value"":""america_slash_indiana_slash_winamac"",""displayOrder"":127,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Indianapolis"",""value"":""america_slash_indianapolis"",""displayOrder"":128,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Iqaluit"",""value"":""america_slash_iqaluit"",""displayOrder"":129,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Kentucky (Louisville)"",""value"":""america_slash_kentucky_slash_louisville"",""displayOrder"":130,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Kentucky (Monticello)"",""value"":""america_slash_kentucky_slash_monticello"",""displayOrder"":131,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Kralendijk"",""value"":""america_slash_kralendijk"",""displayOrder"":132,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America La Paz"",""value"":""america_slash_la_paz"",""displayOrder"":133,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Louisville"",""value"":""america_slash_louisville"",""displayOrder"":134,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Lower Princes"",""value"":""america_slash_lower_princes"",""displayOrder"":135,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Manaus"",""value"":""america_slash_manaus"",""displayOrder"":136,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Marigot"",""value"":""america_slash_marigot"",""displayOrder"":137,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Martinique"",""value"":""america_slash_martinique"",""displayOrder"":138,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Montreal"",""value"":""america_slash_montreal"",""displayOrder"":139,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Montserrat"",""value"":""america_slash_montserrat"",""displayOrder"":140,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Nassau"",""value"":""america_slash_nassau"",""displayOrder"":141,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America New York"",""value"":""america_slash_new_york"",""displayOrder"":142,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Nipigon"",""value"":""america_slash_nipigon"",""displayOrder"":143,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Pangnirtung"",""value"":""america_slash_pangnirtung"",""displayOrder"":144,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Port of Spain"",""value"":""america_slash_port_of_spain"",""displayOrder"":145,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Port-au-Prince"",""value"":""america_slash_port_hyphen_au_hyphen_prince"",""displayOrder"":146,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Porto Velho"",""value"":""america_slash_porto_velho"",""displayOrder"":147,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Puerto Rico"",""value"":""america_slash_puerto_rico"",""displayOrder"":148,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Santiago"",""value"":""america_slash_santiago"",""displayOrder"":149,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Santo Domingo"",""value"":""america_slash_santo_domingo"",""displayOrder"":150,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America St Barthelemy"",""value"":""america_slash_st_barthelemy"",""displayOrder"":151,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America St Kitts"",""value"":""america_slash_st_kitts"",""displayOrder"":152,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America St Lucia"",""value"":""america_slash_st_lucia"",""displayOrder"":153,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America St Thomas"",""value"":""america_slash_st_thomas"",""displayOrder"":154,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America St Vincent"",""value"":""america_slash_st_vincent"",""displayOrder"":155,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Thunder Bay"",""value"":""america_slash_thunder_bay"",""displayOrder"":156,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Toronto"",""value"":""america_slash_toronto"",""displayOrder"":157,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Tortola"",""value"":""america_slash_tortola"",""displayOrder"":158,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 America Virgin"",""value"":""america_slash_virgin"",""displayOrder"":159,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 Brazil West"",""value"":""brazil_slash_west"",""displayOrder"":160,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 Canada Eastern"",""value"":""canada_slash_eastern"",""displayOrder"":161,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 Chile Continental"",""value"":""chile_slash_continental"",""displayOrder"":162,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 US East-Indiana"",""value"":""us_slash_east_hyphen_indiana"",""displayOrder"":163,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 US Eastern"",""value"":""us_slash_eastern"",""displayOrder"":164,""hidden"":false,""readOnly"":true},{""label"":""UTC -04:00 US Michigan"",""value"":""us_slash_michigan"",""displayOrder"":165,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Araguaina"",""value"":""america_slash_araguaina"",""displayOrder"":166,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Buenos Aires)"",""value"":""america_slash_argentina_slash_buenos_aires"",""displayOrder"":167,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Catamarca)"",""value"":""america_slash_argentina_slash_catamarca"",""displayOrder"":168,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (ComodRivadavia)"",""value"":""america_slash_argentina_slash_comodrivadavia"",""displayOrder"":169,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Cordoba)"",""value"":""america_slash_argentina_slash_cordoba"",""displayOrder"":170,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Jujuy)"",""value"":""america_slash_argentina_slash_jujuy"",""displayOrder"":171,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (La Rioja)"",""value"":""america_slash_argentina_slash_la_rioja"",""displayOrder"":172,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Mendoza)"",""value"":""america_slash_argentina_slash_mendoza"",""displayOrder"":173,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Rio Gallegos)"",""value"":""america_slash_argentina_slash_rio_gallegos"",""displayOrder"":174,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Salta)"",""value"":""america_slash_argentina_slash_salta"",""displayOrder"":175,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (San Juan)"",""value"":""america_slash_argentina_slash_san_juan"",""displayOrder"":176,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (San Luis)"",""value"":""america_slash_argentina_slash_san_luis"",""displayOrder"":177,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Tucuman)"",""value"":""america_slash_argentina_slash_tucuman"",""displayOrder"":178,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Argentina (Ushuaia)"",""value"":""america_slash_argentina_slash_ushuaia"",""displayOrder"":179,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Bahia"",""value"":""america_slash_bahia"",""displayOrder"":180,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Belem"",""value"":""america_slash_belem"",""displayOrder"":181,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Buenos Aires"",""value"":""america_slash_buenos_aires"",""displayOrder"":182,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Catamarca"",""value"":""america_slash_catamarca"",""displayOrder"":183,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Cayenne"",""value"":""america_slash_cayenne"",""displayOrder"":184,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Cordoba"",""value"":""america_slash_cordoba"",""displayOrder"":185,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Fortaleza"",""value"":""america_slash_fortaleza"",""displayOrder"":186,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Glace Bay"",""value"":""america_slash_glace_bay"",""displayOrder"":187,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Goose Bay"",""value"":""america_slash_goose_bay"",""displayOrder"":188,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Halifax"",""value"":""america_slash_halifax"",""displayOrder"":189,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Jujuy"",""value"":""america_slash_jujuy"",""displayOrder"":190,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Maceio"",""value"":""america_slash_maceio"",""displayOrder"":191,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Mendoza"",""value"":""america_slash_mendoza"",""displayOrder"":192,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Moncton"",""value"":""america_slash_moncton"",""displayOrder"":193,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Montevideo"",""value"":""america_slash_montevideo"",""displayOrder"":194,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Paramaribo"",""value"":""america_slash_paramaribo"",""displayOrder"":195,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Punta Arenas"",""value"":""america_slash_punta_arenas"",""displayOrder"":196,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Recife"",""value"":""america_slash_recife"",""displayOrder"":197,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Rosario"",""value"":""america_slash_rosario"",""displayOrder"":198,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Santarem"",""value"":""america_slash_santarem"",""displayOrder"":199,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Sao Paulo"",""value"":""america_slash_sao_paulo"",""displayOrder"":200,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 America Thule"",""value"":""america_slash_thule"",""displayOrder"":201,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 Antarctica Palmer"",""value"":""antarctica_slash_palmer"",""displayOrder"":202,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 Antarctica Rothera"",""value"":""antarctica_slash_rothera"",""displayOrder"":203,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 Atlantic Bermuda"",""value"":""atlantic_slash_bermuda"",""displayOrder"":204,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 Atlantic Stanley"",""value"":""atlantic_slash_stanley"",""displayOrder"":205,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 Brazil East"",""value"":""brazil_slash_east"",""displayOrder"":206,""hidden"":false,""readOnly"":true},{""label"":""UTC -03:00 Canada Atlantic"",""value"":""canada_slash_atlantic"",""displayOrder"":207,""hidden"":false,""readOnly"":true},{""label"":""UTC -02:30 America St Johns"",""value"":""america_slash_st_johns"",""displayOrder"":208,""hidden"":false,""readOnly"":true},{""label"":""UTC -02:30 Canada Newfoundland"",""value"":""canada_slash_newfoundland"",""displayOrder"":209,""hidden"":false,""readOnly"":true},{""label"":""UTC -02:00 America Godthab"",""value"":""america_slash_godthab"",""displayOrder"":210,""hidden"":false,""readOnly"":true},{""label"":""UTC -02:00 America Miquelon"",""value"":""america_slash_miquelon"",""displayOrder"":211,""hidden"":false,""readOnly"":true},{""label"":""UTC -02:00 America Noronha"",""value"":""america_slash_noronha"",""displayOrder"":212,""hidden"":false,""readOnly"":true},{""label"":""UTC -02:00 Atlantic South Georgia"",""value"":""atlantic_slash_south_georgia"",""displayOrder"":213,""hidden"":false,""readOnly"":true},{""label"":""UTC -02:00 Brazil DeNoronha"",""value"":""brazil_slash_denoronha"",""displayOrder"":214,""hidden"":false,""readOnly"":true},{""label"":""UTC -01:00 Atlantic Cape Verde"",""value"":""atlantic_slash_cape_verde"",""displayOrder"":215,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Abidjan"",""value"":""africa_slash_abidjan"",""displayOrder"":216,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Accra"",""value"":""africa_slash_accra"",""displayOrder"":217,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Bamako"",""value"":""africa_slash_bamako"",""displayOrder"":218,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Banjul"",""value"":""africa_slash_banjul"",""displayOrder"":219,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Bissau"",""value"":""africa_slash_bissau"",""displayOrder"":220,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Conakry"",""value"":""africa_slash_conakry"",""displayOrder"":221,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Dakar"",""value"":""africa_slash_dakar"",""displayOrder"":222,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Freetown"",""value"":""africa_slash_freetown"",""displayOrder"":223,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Lome"",""value"":""africa_slash_lome"",""displayOrder"":224,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Monrovia"",""value"":""africa_slash_monrovia"",""displayOrder"":225,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Nouakchott"",""value"":""africa_slash_nouakchott"",""displayOrder"":226,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Ouagadougou"",""value"":""africa_slash_ouagadougou"",""displayOrder"":227,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Africa Timbuktu"",""value"":""africa_slash_timbuktu"",""displayOrder"":228,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 America Danmarkshavn"",""value"":""america_slash_danmarkshavn"",""displayOrder"":229,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 America Scoresbysund"",""value"":""america_slash_scoresbysund"",""displayOrder"":230,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Atlantic Azores"",""value"":""atlantic_slash_azores"",""displayOrder"":231,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Atlantic Reykjavik"",""value"":""atlantic_slash_reykjavik"",""displayOrder"":232,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Atlantic St Helena"",""value"":""atlantic_slash_st_helena"",""displayOrder"":233,""hidden"":false,""readOnly"":true},{""label"":""UTC +00:00 Europe Iceland"",""value"":""iceland"",""displayOrder"":234,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Algiers"",""value"":""africa_slash_algiers"",""displayOrder"":235,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Bangui"",""value"":""africa_slash_bangui"",""displayOrder"":236,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Brazzaville"",""value"":""africa_slash_brazzaville"",""displayOrder"":237,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Casablanca"",""value"":""africa_slash_casablanca"",""displayOrder"":238,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Douala"",""value"":""africa_slash_douala"",""displayOrder"":239,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa El Aaiun"",""value"":""africa_slash_el_aaiun"",""displayOrder"":240,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Kinshasa"",""value"":""africa_slash_kinshasa"",""displayOrder"":241,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Lagos"",""value"":""africa_slash_lagos"",""displayOrder"":242,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Libreville"",""value"":""africa_slash_libreville"",""displayOrder"":243,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Luanda"",""value"":""africa_slash_luanda"",""displayOrder"":244,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Malabo"",""value"":""africa_slash_malabo"",""displayOrder"":245,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Ndjamena"",""value"":""africa_slash_ndjamena"",""displayOrder"":246,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Niamey"",""value"":""africa_slash_niamey"",""displayOrder"":247,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Porto-Novo"",""value"":""africa_slash_porto_hyphen_novo"",""displayOrder"":248,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Sao Tome"",""value"":""africa_slash_sao_tome"",""displayOrder"":249,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Africa Tunis"",""value"":""africa_slash_tunis"",""displayOrder"":250,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Atlantic Canary"",""value"":""atlantic_slash_canary"",""displayOrder"":251,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Atlantic Faeroe"",""value"":""atlantic_slash_faeroe"",""displayOrder"":252,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Atlantic Faroe"",""value"":""atlantic_slash_faroe"",""displayOrder"":253,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Atlantic Madeira"",""value"":""atlantic_slash_madeira"",""displayOrder"":254,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe Belfast"",""value"":""europe_slash_belfast"",""displayOrder"":255,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe Dublin"",""value"":""europe_slash_dublin"",""displayOrder"":256,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe Eire"",""value"":""eire"",""displayOrder"":257,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe Guernsey"",""value"":""europe_slash_guernsey"",""displayOrder"":258,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe Isle of Man"",""value"":""europe_slash_isle_of_man"",""displayOrder"":259,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe Jersey"",""value"":""europe_slash_jersey"",""displayOrder"":260,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe Lisbon"",""value"":""europe_slash_lisbon"",""displayOrder"":261,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe London"",""value"":""europe_slash_london"",""displayOrder"":262,""hidden"":false,""readOnly"":true},{""label"":""UTC +01:00 Europe Portugal"",""value"":""portugal"",""displayOrder"":263,""hidden"":false,""readOnly"":true},{""label"":""UTC +0",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Updated by user ID,hs_updated_by_user_id,number,The user who last updated this record. This value is set automatically by HubSpot.,contactinformation,FALSE,,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot, +WhatsApp Phone Number,hs_whatsapp_phone_number,string,The phone number associated with the contact’s WhatsApp account.,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Industry,industry,string,The Industry a contact is in,contactinformation,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Job Title,jobtitle,string,A contact's job title,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last Modified Date,lastmodifieddate,datetime,The date any property on this contact was modified,contactinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last Name,lastname,string,A contact's last name,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Lifecycle Stage,lifecyclestage,enumeration,"The qualification of contacts to sales readiness. It can be set through imports, forms, workflows, and manually on a per contact basis.",contactinformation,TRUE,,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot, +Marketing objective,marketing_objective,enumeration,,contactinformation,FALSE,"[{""label"":""Lead generation"",""value"":""LEAD_GENERATION"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Brand awareness"",""value"":""BRAND_AWARENESS"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Sales"",""value"":""SALES"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Customer engagement"",""value"":""CUSTOMER_ENGAGEMENT"",""displayOrder"":3,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Market expansion"",""value"":""MARKET_EXPANSION"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,Unknown user,Sales view (3); Properties v3 card (1) +Mobile Phone Number,mobilephone,string,A contact's mobile phone number,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of Employees,numemployees,enumeration,The number of company employees,contactinformation,TRUE,"[{""label"":""1-5"",""value"":""1-5"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""5-25"",""value"":""5-25"",""displayOrder"":1,""hidden"":false,""readOnly"":false},{""label"":""25-50"",""value"":""25-50"",""displayOrder"":2,""hidden"":false,""readOnly"":false},{""label"":""50-100"",""value"":""50-100"",""displayOrder"":3,""hidden"":false,""readOnly"":false},{""label"":""100-500"",""value"":""100-500"",""displayOrder"":4,""hidden"":false,""readOnly"":false},{""label"":""500-1000"",""value"":""500-1000"",""displayOrder"":5,""hidden"":false,""readOnly"":false},{""label"":""1000+"",""value"":""1000+"",""displayOrder"":6,""hidden"":false,""readOnly"":false}]",FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Phone Number,phone,string,A contact's primary phone number,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot,Sales view (3); Properties v3 card (1) +Salutation,salutation,string,The title used to address a contact,contactinformation,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +State/Region,state,string,"The contact's state of residence. This might be set via import, form, or integration.",contactinformation,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Timeline,timeline,datetime,,contactinformation,FALSE,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,Unknown user,Sales view (3); Properties v3 card (1) +Twitter Username,twitterhandle,string,The contact's Twitter handle.,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Website URL,website,string,Associated company website.,contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Postal Code,zip,string,"The contact's zip code. This might be set via import, form, or integration.",contactinformation,TRUE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date entered ""Customer (Lifecycle Stage Pipeline)""",hs_v2_date_entered_customer,datetime,"The date and time when the contact entered the 'Customer' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date entered ""Evangelist (Lifecycle Stage Pipeline)""",hs_v2_date_entered_evangelist,datetime,"The date and time when the contact entered the 'Evangelist' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date entered ""Lead (Lifecycle Stage Pipeline)""",hs_v2_date_entered_lead,datetime,"The date and time when the contact entered the 'Lead' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date entered ""Marketing Qualified Lead (Lifecycle Stage Pipeline)""",hs_v2_date_entered_marketingqualifiedlead,datetime,"The date and time when the contact entered the 'Marketing Qualified Lead' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date entered ""Opportunity (Lifecycle Stage Pipeline)""",hs_v2_date_entered_opportunity,datetime,"The date and time when the contact entered the 'Opportunity' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date entered ""Other (Lifecycle Stage Pipeline)""",hs_v2_date_entered_other,datetime,"The date and time when the contact entered the 'Other' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date entered ""Sales Qualified Lead (Lifecycle Stage Pipeline)""",hs_v2_date_entered_salesqualifiedlead,datetime,"The date and time when the contact entered the 'Sales Qualified Lead' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date entered ""Subscriber (Lifecycle Stage Pipeline)""",hs_v2_date_entered_subscriber,datetime,"The date and time when the contact entered the 'Subscriber' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date exited ""Customer (Lifecycle Stage Pipeline)""",hs_v2_date_exited_customer,datetime,"The date and time when the contact exited the 'Customer' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date exited ""Evangelist (Lifecycle Stage Pipeline)""",hs_v2_date_exited_evangelist,datetime,"The date and time when the contact exited the 'Evangelist' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date exited ""Lead (Lifecycle Stage Pipeline)""",hs_v2_date_exited_lead,datetime,"The date and time when the contact exited the 'Lead' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date exited ""Marketing Qualified Lead (Lifecycle Stage Pipeline)""",hs_v2_date_exited_marketingqualifiedlead,datetime,"The date and time when the contact exited the 'Marketing Qualified Lead' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date exited ""Opportunity (Lifecycle Stage Pipeline)""",hs_v2_date_exited_opportunity,datetime,"The date and time when the contact exited the 'Opportunity' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date exited ""Other (Lifecycle Stage Pipeline)""",hs_v2_date_exited_other,datetime,"The date and time when the contact exited the 'Other' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date exited ""Sales Qualified Lead (Lifecycle Stage Pipeline)""",hs_v2_date_exited_salesqualifiedlead,datetime,"The date and time when the contact exited the 'Sales Qualified Lead' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +"Date exited ""Subscriber (Lifecycle Stage Pipeline)""",hs_v2_date_exited_subscriber,datetime,"The date and time when the contact exited the 'Subscriber' stage, 'Lifecycle Stage Pipeline' pipeline",contactlcs,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Member has accessed private content,hs_membership_has_accessed_private_content,number,"1 if a member has accessed any private content, 0 or null if not",contactscripted,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Registered member,hs_registered_member,number,Whether or not a contact is registered,contactscripted,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Facebook click id,hs_facebook_click_id,string,,conversioninformation,FALSE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Google ad click id,hs_google_click_id,string,,conversioninformation,FALSE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +IP Timezone,hs_ip_timezone,string,The timezone reported by a contact's IP address. This is automatically set by HubSpot and can be used for segmentation and reporting.,conversioninformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +IP City,ip_city,string,The city reported by a contact's IP address. This is automatically set by HubSpot and can be used for segmentation and reporting.,conversioninformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +IP Country,ip_country,string,The country reported by a contact's IP address. This is automatically set by HubSpot and can be used for segmentation and reporting.,conversioninformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +IP Country Code,ip_country_code,string,The country code reported by a contact's IP address. This is automatically set by HubSpot and can be used for segmentation and reporting.,conversioninformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +IP State/Region,ip_state,string,The state or region reported by a contact's IP address. This is automatically set by HubSpot and can be used for segmentation and reporting.,conversioninformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +IP State Code/Region Code,ip_state_code,string,The state code or region code reported by a contact's IP address. This is automatically set by HubSpot and can be used for segmentation and reporting.,conversioninformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of Form Submissions,num_conversion_events,number,The number of forms this contact has submitted,conversioninformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Number of Unique Forms Submitted,num_unique_conversion_events,number,The number of different forms this contact has submitted,conversioninformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Recent Conversion Date,recent_conversion_date,datetime,The date this contact last submitted a form,conversioninformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Recent Conversion,recent_conversion_event_name,string,The last form this contact submitted,conversioninformation,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +Close Date,closedate,datetime,Date the contact became a customer. Set automatically when a deal or opportunity is marked as closed-won. It can also be set manually or programmatically.,deal_information,FALSE,,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Days To Close,days_to_close,number,Count of days elapsed between creation and being closed as a customer. Set automatically.,deal_information,FALSE,,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,HubSpot, +First Deal Created Date,first_deal_created_date,datetime,Date first deal was created for contact. Set automatically.,deal_information,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Buying Role,hs_buying_role,enumeration,"Role the contact plays during the sales process. Contacts can have multiple roles, and can share roles with others.",deal_information,TRUE,"[{""label"":""Blocker"",""value"":""BLOCKER"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Budget Holder"",""value"":""BUDGET_HOLDER"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Champion"",""value"":""CHAMPION"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Decision Maker"",""value"":""DECISION_MAKER"",""displayOrder"":3,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""End User"",""value"":""END_USER"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Executive Sponsor"",""value"":""EXECUTIVE_SPONSOR"",""displayOrder"":5,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Influencer"",""value"":""INFLUENCER"",""displayOrder"":6,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Legal & Compliance"",""value"":""LEGAL_AND_COMPLIANCE"",""displayOrder"":7,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Other"",""value"":""OTHER"",""displayOrder"":8,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Number of Associated Deals,num_associated_deals,number,Count of deals associated with this contact. Set automatically by HubSpot.,deal_information,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Recent Deal Amount,recent_deal_amount,number,Amount of last closed won deal associated with a contact. Set automatically.,deal_information,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Recent Deal Close Date,recent_deal_close_date,datetime,Date last deal associated with contact was closed-won. Set automatically.,deal_information,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Total Revenue,total_revenue,number,Sum of all closed-won deal revenue associated with the contact. Set automatically.,deal_information,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Invalid email address,hs_email_bad_address,bool,The email address associated with this contact is invalid.,emailinformation,FALSE,"[{""label"":""True"",""value"":""true"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""False"",""value"":""false"",""displayOrder"":1,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Marketing emails bounced,hs_email_bounce,number,The number of marketing emails that bounced for the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Marketing emails clicked,hs_email_click,number,The number of marketing emails which have had link clicks for the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Email address quarantine reason,hs_email_customer_quarantined_reason,enumeration,The reason why the email address has been quarantined.,emailinformation,FALSE,"[{""label"":""Suspension Remediation"",""value"":""SUSPENSION_REMEDIATION"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Blocklist Remediation"",""value"":""BLOCKLIST_REMEDIATION"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Trust & Safety Remediation"",""value"":""TRUST_SAFETY_REMEDIATION"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Marketing emails delivered,hs_email_delivered,number,The number of marketing emails delivered for the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First marketing email click date,hs_email_first_click_date,datetime,The date of the earliest link click for any marketing email to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First marketing email open date,hs_email_first_open_date,datetime,The date of the earliest open for any marketing email to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First marketing email reply date,hs_email_first_reply_date,datetime,The date of the earliest reply for any marketing email to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First marketing email send date,hs_email_first_send_date,datetime,The date of the earliest delivery for any marketing email to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Email hard bounce reason,hs_email_hard_bounce_reason_enum,enumeration,"The issue that caused a contact to hard bounce from your emails. If this is an error or a temporary issue, you can unbounce this contact from the contact record.",emailinformation,FALSE,"[{""label"":""Content"",""value"":""CONTENT"",""displayOrder"":0,""hidden"":false,""description"":""Content in the body or subject of an email triggered a spam filter"",""readOnly"":true},{""label"":""Mailbox full"",""value"":""MAILBOX_FULL"",""displayOrder"":1,""hidden"":false,""description"":""The recipient's inbox is full or no longer in use"",""readOnly"":true},{""label"":""Other"",""value"":""OTHER"",""displayOrder"":2,""hidden"":false,""description"":""An unknown reason caused the contact to hard bounce"",""readOnly"":true},{""label"":""Policy"",""value"":""POLICY"",""displayOrder"":3,""hidden"":false,""description"":""An email violated the recipient’s security policy"",""readOnly"":true},{""label"":""Spam"",""value"":""SPAM"",""displayOrder"":4,""hidden"":false,""description"":""An email was marked as spam by the recipient or their inbox provider"",""readOnly"":true},{""label"":""Unknown user"",""value"":""UNKNOWN_USER"",""displayOrder"":5,""hidden"":false,""description"":""A permanent error, such as an unknown email address"",""readOnly"":true}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last marketing email click date,hs_email_last_click_date,datetime,The date of the most recent link click for any marketing email to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last marketing email name,hs_email_last_email_name,string,The name of the last marketing email sent to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last marketing email open date,hs_email_last_open_date,datetime,The date of the most recent open for any marketing email to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last marketing email reply date,hs_email_last_reply_date,datetime,The date of the latest reply for any marketing email to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Last marketing email send date,hs_email_last_send_date,datetime,The date of the most recent delivery for any marketing email to the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Marketing emails opened,hs_email_open,number,The number of marketing emails opened for the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Unsubscribed from all email,hs_email_optout,bool,Indicates that the current email address has opted out of all email.,emailinformation,FALSE,"[{""label"":""True"",""value"":""true"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""False"",""value"":""false"",""displayOrder"":1,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Opted out of email: Marketing Information,hs_email_optout_712798768,enumeration,Indicates that the current email address has opted out of this email type.,emailinformation,FALSE,"[{""label"":""Yes"",""value"":""true"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""No"",""value"":""false"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Opted out of email: One to One,hs_email_optout_712798769,enumeration,Indicates that the current email address has opted out of this email type.,emailinformation,FALSE,"[{""label"":""Yes"",""value"":""true"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""No"",""value"":""false"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Email Address Quarantined,hs_email_quarantined,bool,Indicates that the current email address has been quarantined for anti-abuse reasons and any marketing email sends to it will be blocked. This is automatically set by HubSpot.,emailinformation,FALSE,"[{""label"":""True"",""value"":""true"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""False"",""value"":""false"",""displayOrder"":1,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Email address automated quarantine reason,hs_email_quarantined_reason,enumeration,The automated reason why the email address has been quarantined.,emailinformation,FALSE,"[{""label"":""Suspicious List Import"",""value"":""ON_LIST_IMPORT"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""High Hard Bounce Rate"",""value"":""HIGH_HARD_BOUNCE_RATE"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Multiple Cancelled Campaigns"",""value"":""MULTIPLE_CANCELLED_CAMPAIGNS"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Suspicious Form Activity"",""value"":""FORM_ABUSE"",""displayOrder"":3,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Recipient Complaint"",""value"":""RECIPIENT_COMPLAINT"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Suspension Remediation"",""value"":""SUSPENSION_REMEDIATION"",""displayOrder"":5,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Blocklist Remediation"",""value"":""BLOCKLIST_REMEDIATION"",""displayOrder"":6,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Trust & Safety Remediation"",""value"":""TRUST_SAFETY_REMEDIATION"",""displayOrder"":7,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Other"",""value"":""OTHER"",""displayOrder"":8,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Undeliverable Email Address"",""value"":""UNDELIVERABLE_ADDRESS"",""displayOrder"":9,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Microsoft Undeliverable - Microsoft reports that address is inactive"",""value"":""MICROSOFT_UNDELIVERABLE"",""displayOrder"":10,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Marketing emails replied,hs_email_replied,number,The number of marketing emails replied to by the current email address. This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Sends Since Last Engagement,hs_email_sends_since_last_engagement,number,The number of marketing emails that have been sent to the current email address since the last engagement (open or link click). This is automatically set by HubSpot.,emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Marketing email confirmation status,hs_emailconfirmationstatus,enumeration,The status of a contact's eligibility to receive marketing email. This is automatically set by HubSpot.,emailinformation,FALSE,"[{""label"":""Confirmed"",""value"":""Confirmed"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""Confirmation Pending"",""value"":""Confirmation Pending"",""displayOrder"":1,""hidden"":false,""readOnly"":false},{""label"":""Confirmation Email Sent"",""value"":""Confirmation Email Sent"",""displayOrder"":2,""hidden"":false,""readOnly"":false},{""label"":""User clicked confirmation"",""value"":""User clicked confirmation"",""displayOrder"":3,""hidden"":false,""readOnly"":false},{""label"":""HubSpot Rep. marked confirmed"",""value"":""HubSpot Rep. marked confirmed"",""displayOrder"":4,""hidden"":false,""readOnly"":false},{""label"":""Customer marked confirmed"",""value"":""Customer marked confirmed"",""displayOrder"":5,""hidden"":false,""readOnly"":false},{""label"":""Confirmed from previous behavior"",""value"":""Confirmed from previous behavior"",""displayOrder"":6,""hidden"":false,""readOnly"":false},{""label"":""Confirmed due to form"",""value"":""Confirmed due to form"",""displayOrder"":7,""hidden"":false,""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Enriched Email Bounce Detected,hs_enriched_email_bounce_detected,bool,Bounce Detected attribute is populated when Hubspot & it’s partners identify the email may not be deliverable or is no longer valid for that contact.,emailinformation,FALSE,"[{""label"":""Yes"",""value"":""true"",""displayOrder"":0,""hidden"":false,""readOnly"":false},{""label"":""No"",""value"":""false"",""displayOrder"":1,""hidden"":false,""readOnly"":false}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Legal basis for processing contact's data,hs_legal_basis,enumeration,Legal basis for processing contact's data; 'Not applicable' will exempt the contact from GDPR protections,emailinformation,FALSE,"[{""label"":""Legitimate interest - Lead"",""value"":""Legitimate interest – prospect/lead"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Legitimate interest - Customer"",""value"":""Legitimate interest – existing customer"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Legitimate interest - Other"",""value"":""Legitimate interest - other"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Performance of a contract"",""value"":""Performance of a contract"",""displayOrder"":3,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Freely given consent from contact"",""value"":""Freely given consent from contact"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true},{""label"":""Not applicable"",""value"":""Not applicable"",""displayOrder"":5,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":true}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Quarantined Emails,hs_quarantined_emails,string,"Lists all emails associated with this contact that have been quarantined, with a source and reason.",emailinformation,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Company size,company_size,string,Contact's company size. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Date of birth,date_of_birth,string,Contact's date of birth. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Degree,degree,string,Contact's degree. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Field of study,field_of_study,string,Contact's field of study. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Gender,gender,string,Contact's gender. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Graduation date,graduation_date,string,Contact's graduation date. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Job function,job_function,string,Contact's job function. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Marital Status,marital_status,string,Contact's marital status. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Military status,military_status,string,Contact's military status. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Relationship Status,relationship_status,string,Contact's relationship status. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +School,school,string,Contact's school. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Seniority,seniority,string,Contact's seniority. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Start date,start_date,string,Contact's start date. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Work email,work_email,string,Contact's work email. Required for the Facebook Ads Integration. Automatically synced from the Lead Ads tool.,facebook_ads_properties,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First Closed Order ID,hs_first_closed_order_id,number,The id of the associated order that was first to be closed,order_information,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +First Order Closed Date,hs_first_order_closed_date,datetime,Date first order was closed. Set Automatically,order_information,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Recent Closed Order Date,hs_recent_closed_order_date,datetime,Date last order was closed. Set automatically.,order_information,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Contact unworked,hs_is_unworked,bool,Contact has not been assigned or has not been engaged after last owner assignment/re-assignment,sales_properties,FALSE,"[{""label"":""True"",""value"":""true"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""False"",""value"":""false"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Lead Status,hs_lead_status,enumeration,"The contact's sales, prospecting or outreach status",sales_properties,TRUE,"[{""label"":""New"",""value"":""NEW"",""displayOrder"":0,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Open"",""value"":""OPEN"",""displayOrder"":1,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""In Progress"",""value"":""IN_PROGRESS"",""displayOrder"":2,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Open Deal"",""value"":""OPEN_DEAL"",""displayOrder"":3,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Unqualified"",""value"":""UNQUALIFIED"",""displayOrder"":4,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Attempted to Contact"",""value"":""ATTEMPTED_TO_CONTACT"",""displayOrder"":5,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Connected"",""value"":""CONNECTED"",""displayOrder"":6,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false},{""label"":""Bad Timing"",""value"":""BAD_TIMING"",""displayOrder"":7,""doubleData"":0.0,""hidden"":false,""description"":"""",""readOnly"":false}]",FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot,Sales view (3); Properties v3 card (1) +Owner assigned date,hubspot_owner_assigneddate,datetime,The most recent timestamp of when an owner was assigned to this record. This value is set automatically by HubSpot.,sales_properties,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +Contact owner,hubspot_owner_id,enumeration,"The owner of a contact. This can be any HubSpot user or Salesforce integration user, and can be set manually or via Workflows.",sales_properties,FALSE,,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot,Sales view (3); Properties v3 card (1) +HubSpot Team,hubspot_team_id,enumeration,The team of the owner of a contact.,sales_properties,FALSE,,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,HubSpot, +HubSpot Score,hubspotscore,number,The number that shows qualification of contacts to sales readiness. It can be set in HubSpot's Lead Scoring app.,sales_properties,FALSE,,TRUE,TRUE,FALSE,FALSE,FALSE,TRUE,HubSpot, +LinkedIn URL,hs_linkedin_url,string,The URL of the contact's LinkedIn page.,socialmediainformation,TRUE,,FALSE,FALSE,FALSE,FALSE,FALSE,TRUE,HubSpot, diff --git a/level_1/hubspot_agent/hubspot/hubspot_tool.py b/level_1/hubspot_agent/hubspot/hubspot_tool.py new file mode 100644 index 0000000..4688058 --- /dev/null +++ b/level_1/hubspot_agent/hubspot/hubspot_tool.py @@ -0,0 +1,151 @@ +import requests +import json +import datetime +import keyring +from hubspot.config import Config +from hubspot.postgres_client import PostgresClient + + +class HubSpotTool(): + """Class to interact with the HubSpot API""" + + def __init__(self, base_url=None): + self.BASE_URL = base_url or Config.hubspot["base_url"] + + # Get configuration from config.py + service_id = Config.hubspot["service_id"] + access_token_key = Config.hubspot["access_token_key"] + db_user_key = Config.hubspot["db_user_key"] + db_password_key = Config.hubspot["db_password_key"] + + # Get HubSpot access token and database credentials from keyring + self.hubspot_access_token = keyring.get_password(service_id, access_token_key) + db_user = keyring.get_password(service_id, db_user_key) + db_password = keyring.get_password(service_id, db_password_key) + + # Get database configuration from config file + db_name = Config.database["name"] + db_host = Config.database["host"] + db_port = Config.database["port"] + + self.db_client = PostgresClient( + db_name=db_name, + db_user=db_user, + db_password=db_password, + db_host=db_host, + db_port=db_port + ) + + def __destruct__(self): + # Close the database connection pool + self.db_client.close() + + def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): + """Helper function to make API calls using Bearer Token""" + access_token = self.hubspot_access_token + print("Calling ", endpoint, " method ", method, " with payload \n", data) + + url = f"{self.BASE_URL}{endpoint}" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {access_token}" + } + + print(f"Making {method} request to {url}") + if params: + print(f"Query parameters: {params}") + if data: + print(f"Request body: {json.dumps(data, indent=2)}") + + try: + if method == 'POST': + response = requests.post(url, headers=headers, json=data, params=params) + elif method == 'PATCH': + response = requests.patch(url, headers=headers, json=data, params=params) + elif method == 'GET': + response = requests.get(url, headers=headers, params=params) + elif method == 'DELETE': + response = requests.delete(url, headers=headers, params=params) + else: + raise ValueError(f"Unsupported HTTP method: {method}") + + response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx) + + print(f"Response Status Code: {response.status_code}") + # Attempt to parse JSON response if content exists + if response.content: + try: + return response.json() + except json.JSONDecodeError: + print("Warning: Response was not JSON.") + return response.text # Return plain text if not JSON + else: + print("Info: Response body is empty.") + return None # No content in response + + except requests.exceptions.RequestException as e: + print(f"API Request failed: {e}") + if hasattr(e, 'response') and e.response is not None: + print(f"Status Code: {e.response.status_code}") + print(f"Response Body: {e.response.text}") + return None + + def delete_lead(self, lead_id): + """Delete a lead from HubSpot""" + delete_lead_endpoint = f"/crm/v3/objects/contacts/{lead_id}" + return self.call_hubspot_api_oauth(method='DELETE', endpoint=delete_lead_endpoint) + + def create_lead(self, lead_data: dict): + + # Example 1: Creating a new lead + print("\n--- Creating a New Lead (Contact) using Private App Token ---") + print(f"Lead data received: {json.dumps(lead_data, indent=2)}") + """Create a new lead in HubSpot""" + create_lead_endpoint = "/crm/v3/objects/contacts" + created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=lead_data) + + if created_lead_response: + print("Lead created successfully using Private App Token:") + print(json.dumps(created_lead_response, indent=2)) + new_lead_id = created_lead_response.get('id') + print(f"Newly created lead ID: {new_lead_id}") + + try: + # Store the lead in PostgreSQL if created successfully + db_id = self.db_client.store_lead(hubspot_id=new_lead_id, lead_data=lead_data) + finally: + return new_lead_id + else: + print("Failed to create lead using Private App Token.") + return None + + def update_lead(self, properties: json): + lead_id = properties.get('lead_id') + """Update an existing lead in HubSpot""" + modify_lead_endpoint = f"/crm/v3/objects/contacts/{lead_id}" + return self.call_hubspot_api_oauth('PATCH', modify_lead_endpoint, data=properties) + + def create_meeting(self, meeting_data: dict): + """Create a new meeting in HubSpot""" + create_meeting_endpoint = "/crm/v3/objects/meetings" + response = self.call_hubspot_api_oauth('POST', create_meeting_endpoint, data=meeting_data) + meeting_id = response.get('meeting_id') + lead_id = response.get('lead_id') + if meeting_id: + db_meeting_id = self.db_client.store_meeting( + hubspot_id=meeting_id, + lead_id=lead_id, + title=meeting_data["properties"]["hs_meeting_title"], + start_time=meeting_data["properties"]["start_time"], + end_time=meeting_data["properties"]["end_time"] + ) + + return response + + +hstool = HubSpotTool() + + +def create_lead(json_payload: dict): + print('Agent called "create_lead()"') + return hstool.create_lead(json_payload) diff --git a/level_1/hubspot_agent/hubspot/postgres_client.py b/level_1/hubspot_agent/hubspot/postgres_client.py new file mode 100644 index 0000000..e18900b --- /dev/null +++ b/level_1/hubspot_agent/hubspot/postgres_client.py @@ -0,0 +1,125 @@ +import psycopg2 +from psycopg2 import pool + + +class PostgresClient: + """Class to handle PostgreSQL database operations""" + + def __init__(self, db_name, db_user, db_password, db_host, db_port, min_conn=1, max_conn=10): + self.conn_pool = psycopg2.pool.SimpleConnectionPool( + minconn=min_conn, + maxconn=max_conn, + database=db_name, + user=db_user, + password=db_password, + host=db_host, + port=db_port + ) + self._init_db() + + def _init_db(self): + """Initialize database tables if they don't exist""" + conn = self.conn_pool.getconn() + try: + with conn.cursor() as cursor: + cursor.execute(""" + CREATE TABLE IF NOT EXISTS hubspot_leads ( + id SERIAL PRIMARY KEY, + hubspot_id VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL, + firstname VARCHAR(255), + lastname VARCHAR(255), + phone VARCHAR(255), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ) + """) + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS hubspot_meetings ( + id SERIAL PRIMARY KEY, + hubspot_id VARCHAR(255) NOT NULL UNIQUE, + lead_id VARCHAR(255) NOT NULL, + title VARCHAR(255), + start_time TIMESTAMP WITH TIME ZONE, + end_time TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + FOREIGN KEY (lead_id) REFERENCES hubspot_leads(hubspot_id) + ) + """) + conn.commit() + except Exception as e: + conn.rollback() + print(f"Database initialization error: {e}") + finally: + self.conn_pool.putconn(conn) + + def store_lead(self, hubspot_id, lead_data): + """Store a new lead in the database""" + conn = self.conn_pool.getconn() + email = lead_data['properties']['email'] + firstname = lead_data['properties'].get('firstname') # Use .get for optional fields + lastname = lead_data['properties'].get('lastname') + phone = lead_data['properties'].get('phone') + try: + with conn.cursor() as cursor: + cursor.execute( + """ + INSERT INTO hubspot_leads (hubspot_id, email, firstname, lastname, phone) + VALUES (%s, %s, %s, %s, %s) + RETURNING id; + """, + (hubspot_id, email, phone, firstname, lastname) + ) + result = cursor.fetchone() + conn.commit() + return result[0] if result else None + except Exception as e: + conn.rollback() + print(f"Error storing lead: {e}") + return None + finally: + self.conn_pool.putconn(conn) + + def store_meeting(self, hubspot_id, lead_id, title, start_time, end_time): + """Store a new meeting in the database""" + conn = self.conn_pool.getconn() + try: + with conn.cursor() as cursor: + cursor.execute( + """ + INSERT INTO hubspot_meetings (hubspot_id, lead_id, title, start_time, end_time) + VALUES (%s, %s, %s, %s, %s) + RETURNING id + """, + (hubspot_id, lead_id, title, start_time, end_time) + ) + result = cursor.fetchone() + conn.commit() + return result[0] if result else None + except Exception as e: + conn.rollback() + print(f"Error storing meeting: {e}") + return None + finally: + self.conn_pool.putconn(conn) + + def get_lead_by_hubspot_id(self, hubspot_id): + """Retrieve a lead by HubSpot ID""" + conn = self.conn_pool.getconn() + try: + with conn.cursor() as cursor: + cursor.execute( + """ + SELECT * FROM hubspot_leads WHERE hubspot_id = %s + """, + (hubspot_id,) + ) + result = cursor.fetchone() + return result + finally: + self.conn_pool.putconn(conn) + + def close(self): + """Close the connection pool""" + if self.conn_pool: + self.conn_pool.closeall() diff --git a/level_1/hubspot_agent/requirements.txt b/level_1/hubspot_agent/requirements.txt new file mode 100644 index 0000000..35343c8 --- /dev/null +++ b/level_1/hubspot_agent/requirements.txt @@ -0,0 +1,8 @@ +google-adk +google-search-results +keyring +psycopg2 +requests +json +datetime +getpass \ No newline at end of file From 0baea8dfd8146d81db72ea0a288de8b7048a49db Mon Sep 17 00:00:00 2001 From: rcmohan Date: Mon, 5 May 2025 00:43:43 -0400 Subject: [PATCH 02/11] Added instructions to run the agent. --- level_1/hubspot_agent/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 level_1/hubspot_agent/README.md diff --git a/level_1/hubspot_agent/README.md b/level_1/hubspot_agent/README.md new file mode 100644 index 0000000..8d24b60 --- /dev/null +++ b/level_1/hubspot_agent/README.md @@ -0,0 +1,19 @@ +## HubSpot Agent using Google ADK +This agent integrates with HubSpot using HubSpot APIs, and is capable of automatically parsing emails to manage contacts using the email content. + +### Key Features +The agent can create a new lead in HubSpot extracting the contents of the email, update the info about the lead if already existing, set up meetings with leads and other basic activities. + +### Implementation Details +The agent was implemented using Google ADK and tested with Gemini 2.0 Flash as the LLM. The agent, defined in `hubspot/agent.py`, contains the prompt and refers to the HubSpot client tool. The tool, defined in `hubspot/hubspot_tool.py`, invokes HubSpot API to manage leads and meetings. The tool also stores the `lead_id` created by HubSpot, along with `name` and `email` of the lead. + +### How to run +The setup is similar to as defined in [Google ADK Quickstart guide](https://google.github.io/adk-docs/get-started/quickstart/). +1. Run `python -m env .env` from the project root folder (folder named `hubspot_agent` if you checked out the git project) +2. Run `python hubspot/Secrets.py` to setup your HubSpot and DB credentials +3. Update `hubspot/.env` file with your Google API key (you can create one [here](https://aistudio.google.com/app/apikey)) +4. Start the agent with `adk web` from the project root folder +5. Open [http://localhost:8000](http://localhost:8000) from your browser + +### Common errors +1. If you have trouble running on Windows machines, try an alternate port. For instance, you can use port `8080` with command `adk web --port 8080`. Navigate to [http://localhost:8080](http://localhost:8080) \ No newline at end of file From c7a760d0158f7e2351a7d6147e0028a91eada74f Mon Sep 17 00:00:00 2001 From: rcmohan Date: Sun, 18 May 2025 22:38:52 -0400 Subject: [PATCH 03/11] Added code to run the agent without Google ADK --- level_1/hubspot_agent/README.md | 101 ++++++++++-- level_1/hubspot_agent/adk_agent.py | 126 +++++++++++++++ level_1/hubspot_agent/hubspot/Secrets.py | 11 +- level_1/hubspot_agent/hubspot/__init__.py | 2 +- level_1/hubspot_agent/hubspot/agent.py | 20 ++- level_1/hubspot_agent/hubspot/config.py | 29 +++- .../hubspot/hubspot_templates.py | 149 ++++++++++++++++++ level_1/hubspot_agent/hubspot/hubspot_tool.py | 115 ++++++++++++-- .../hubspot_agent/hubspot/langfuse_config.py | 12 ++ .../hubspot_agent/hubspot/postgres_client.py | 4 +- level_1/hubspot_agent/requirements.txt | 7 +- 11 files changed, 525 insertions(+), 51 deletions(-) create mode 100644 level_1/hubspot_agent/adk_agent.py create mode 100644 level_1/hubspot_agent/hubspot/hubspot_templates.py create mode 100644 level_1/hubspot_agent/hubspot/langfuse_config.py diff --git a/level_1/hubspot_agent/README.md b/level_1/hubspot_agent/README.md index 8d24b60..b3520b5 100644 --- a/level_1/hubspot_agent/README.md +++ b/level_1/hubspot_agent/README.md @@ -1,19 +1,90 @@ -## HubSpot Agent using Google ADK -This agent integrates with HubSpot using HubSpot APIs, and is capable of automatically parsing emails to manage contacts using the email content. +# Project Title -### Key Features -The agent can create a new lead in HubSpot extracting the contents of the email, update the info about the lead if already existing, set up meetings with leads and other basic activities. +This is an implementation of an AI agent (backed by LLM) that uses Google ADK and an LLM to parse an email, identify if it is a lead, and adds it to HubSpot. It also uses Postgres database to maintain leads and uses Langfuse for logging and tracing -### Implementation Details -The agent was implemented using Google ADK and tested with Gemini 2.0 Flash as the LLM. The agent, defined in `hubspot/agent.py`, contains the prompt and refers to the HubSpot client tool. The tool, defined in `hubspot/hubspot_tool.py`, invokes HubSpot API to manage leads and meetings. The tool also stores the `lead_id` created by HubSpot, along with `name` and `email` of the lead. +## Prerequisites -### How to run -The setup is similar to as defined in [Google ADK Quickstart guide](https://google.github.io/adk-docs/get-started/quickstart/). -1. Run `python -m env .env` from the project root folder (folder named `hubspot_agent` if you checked out the git project) -2. Run `python hubspot/Secrets.py` to setup your HubSpot and DB credentials -3. Update `hubspot/.env` file with your Google API key (you can create one [here](https://aistudio.google.com/app/apikey)) -4. Start the agent with `adk web` from the project root folder -5. Open [http://localhost:8000](http://localhost:8000) from your browser +Before you begin, ensure you have the following installed: -### Common errors -1. If you have trouble running on Windows machines, try an alternate port. For instance, you can use port `8080` with command `adk web --port 8080`. Navigate to [http://localhost:8080](http://localhost:8080) \ No newline at end of file +* Python 3.8 or higher +* pip (Python package installer) +* Docker and Docker Compose +* keyring (for secure credential storage) + +## Setup + +1. **Clone the repository (if applicable):** + + ```bash + # Replace with your repository URL + # git clone + # cd + ``` + +2. **Create and activate a virtual environment:** + + ```bash + python -m venv .venv + # On Windows: + # .venv\Scripts\activate + # On macOS/Linux: + # source .venv/bin/activate + ``` + +3. **Install dependencies:** + + ```bash + pip install -r requirements.txt + ``` + +4. **Set up secure credentials:** + + This project uses `keyring` to securely store sensitive information like API tokens and database passwords. Run the `Secrets.py` script to set up your credentials. + + ```bash + python hubspot/Secrets.py + ``` + + Follow the prompts to enter your HubSpot access token and database credentials. These will be stored securely by your operating system's credential manager. + +5. **Set up local Langfuse and PostgreSQL:** + + This project uses a local instance of Langfuse and PostgreSQL for observability and data storage, managed by Docker Compose. Ensure Docker is running. + + Create a `.env` file in the project root directory with the following content (adjust values as needed, but match those used in `docker-compose.yml` and `hubspot/langfuse_config.py` for the local setup): + + ```dotenv + POSTGRES_USER= + POSTGRES_PASSWORD= + POSTGRES_DB= + ``` + + Start the Docker containers: + + ```bash + docker-compose up -d + ``` + + This will start the PostgreSQL database and the Langfuse server. You can access the Langfuse UI at `http://localhost:3000`. + +## Running the Agent + +1. Ensure your virtual environment is active. +2. Ensure your local Langfuse and PostgreSQL containers are running (`docker-compose up -d`). +3. Two options to run the agent + * **Using the `adk web` command:** + ``` + adk web --port 8080 + ``` + The agent web server will start and be accessible at `http://localhost:8080`. + + * **Using the adk app** + ``` + uvicorn adk_agent:app --reload + ``` + +## Langfuse Observability + +This project is integrated with Langfuse to provide observability for the agent's behavior and interactions with external services (like HubSpot). All traces and logs will be sent to your local Langfuse instance. + +* View traces, spans, and logs in the Langfuse UI at `http://localhost:3000`. diff --git a/level_1/hubspot_agent/adk_agent.py b/level_1/hubspot_agent/adk_agent.py new file mode 100644 index 0000000..1b47211 --- /dev/null +++ b/level_1/hubspot_agent/adk_agent.py @@ -0,0 +1,126 @@ +import dotenv # Good practice if your config relies on .env files +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, EmailStr, Field # For data validation +from typing import Optional +import datetime + +# Assuming hubspot.agent and hubspot.config are your custom modules +from hubspot.agent import root_agent, system_prompt +from hubspot.config import Config + +import vertexai +from vertexai.preview.reasoning_engines import AdkApp + +# Initialize Vertex AI (ensure Config.gcp["gcp_project"] is correctly loaded) +# It's good practice to load .env files before accessing Config if it uses them +dotenv.load_dotenv() # Load .env file if present + +try: + vertexai.init(project=Config.gcp["gcp_project"]) +except Exception as e: + print(f"Error initializing Vertex AI: {e}") + # Depending on your needs, you might want to exit or handle this gracefully + # For now, we'll let it proceed so FastAPI can start, but agent calls might fail. + +app = FastAPI( + title="ADK Agent API", + description="API to interact with the ADK agent for chat and email processing.", + version="1.0.0" +) + +# --- Pydantic Model for Incoming Email Data --- +class EmailContent(BaseModel): + sender: EmailStr = Field(..., example="sender@example.com", description="Email address of the sender.") + recipient: Optional[EmailStr] = Field(None, example="recipient@example.com", description="Email address of the recipient (if available).") + subject: str = Field(..., example="Important Update", description="Subject line of the email.") + body: str = Field(..., example="Hello team, here's an update...", description="Main content/body of the email.") + received_at: Optional[datetime.datetime] = Field(None, example="2024-05-18T14:30:00Z", description="Timestamp when the email was received.") + message_id: Optional[str] = Field(None, example="", description="Unique identifier for the email message.") + + class Config: + # This allows FastAPI to show example values in the documentation + json_schema_extra = { + "example": { + "sender": "jane.doe@example.com", + "recipient": "support@example.com", + "subject": "Inquiry about product X", + "body": "Dear Support Team,\n\nI have a question regarding product X. Could you please provide more details on its features?\n\nThanks,\nJane", + "received_at": "2024-05-18T10:00:00Z", + "message_id": "" + } + } + +# Initialize ADK App with your root agent +# Ensure root_agent is correctly defined and imported +try: + adk_app = AdkApp(agent=root_agent) +except Exception as e: + print(f"Error initializing AdkApp with root_agent: {e}") + # Handle this critical error appropriately, as the app won't function + adk_app = None # Set to None to prevent further errors if initialization fails + + + +@app.post("/chat") +async def chat_with_agent(query: str): + + session = adk_app.create_session() + response_events = [] + async for event in session.execute(input={"message": query}): + response_events.append(event) + if event.name == "agent:llm:final_response": + return {"response": event.data.get("text")} + return {"response_events": response_events} # Fallback + + +# --- New Endpoint for Processing Email Content --- +@app.post("/process-email", tags=["Agent Interaction"]) +async def process_email_with_agent(email_data: EmailContent): + if not adk_app: + raise HTTPException(status_code=500, detail="ADK Application not initialized.") + + try: + session = adk_app.create_session() # Consider if session management is needed per email or per sender + response_events = [] + final_text_response = None + + async for event in session.execute(input={"message": system_prompt}): + response_events.append(event.model_dump_json()) # Storing JSON serializable event data + if event.name == "agent:llm:final_response": + final_text_response = event.data.get("text") + return { + "status": "Email processed", + "agent_response": final_text_response, + "original_subject": email_data.subject, + "original_sender": email_data.sender, + "session_id": session.session_id + } + + # Fallback if no 'agent:llm:final_response' event with text was found + # This logic mirrors the /chat endpoint's fallback + if final_text_response: + return {"status": "Email processed", "agent_response": final_text_response, "session_id": session.session_id} + + return { + "status": "Email processed, but no direct text response from agent.", + "agent_response_summary": "Agent processed the email content.", + "all_events": response_events, # Be cautious with returning all events in production + "session_id": session.session_id + } + + except Exception as e: + # Log the exception for debugging + print(f"Error processing email with agent: {e}") + raise HTTPException(status_code=500, detail=f"Error during email processing with agent: {str(e)}") + +# --- Optional: Root endpoint for health check or API info --- +@app.get("/", tags=["General"]) +async def read_root(): + """ + Root endpoint providing basic API information. + """ + return { + "message": "Welcome to the ADK Agent API", + "docs_url": "/docs", + "redoc_url": "/redoc" + } \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/Secrets.py b/level_1/hubspot_agent/hubspot/Secrets.py index 8bb3c58..35a228d 100644 --- a/level_1/hubspot_agent/hubspot/Secrets.py +++ b/level_1/hubspot_agent/hubspot/Secrets.py @@ -1,4 +1,5 @@ + # !/usr/bin/env python3 """ Setup script to initialize keyring with necessary credentials @@ -40,8 +41,8 @@ def setup_credentials(self): db_password = getpass.getpass("Enter database password: ") # Store credentials in keyring - keyring.set_password(service_id, Config.hubspot["db_user_key"], db_user) - keyring.set_password(service_id, Config.hubspot["db_password_key"], db_password) + keyring.set_password(service_id, Config.database["db_user_key"], db_user) + keyring.set_password(service_id, Config.database["db_password_key"], db_password) print("\nCredentials stored successfully!") print("You can now run the main program.") @@ -57,13 +58,13 @@ def test_credentials(self): return False, "HubSpot access token not found" # Test database credentials - db_user = keyring.get_password(service_id, Config.hubspot["db_user_key"]) + db_user = keyring.get_password(service_id, Config.database["db_user_key"]) if not db_user: return False, "Database username not found" # else: # print(db_user) - db_password = keyring.get_password(service_id, Config.hubspot["db_password_key"]) + db_password = keyring.get_password(service_id, Config.database["db_password_key"]) if not db_password: return False, "Database password not found" @@ -84,4 +85,4 @@ def test_credentials(self): print("Keeping existing credentials.") else: print(f"Missing credentials: {message}") - setup.setup_credentials() + setup.setup_credentials() \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/__init__.py b/level_1/hubspot_agent/hubspot/__init__.py index 02c597e..63bd45e 100644 --- a/level_1/hubspot_agent/hubspot/__init__.py +++ b/level_1/hubspot_agent/hubspot/__init__.py @@ -1 +1 @@ -from . import agent +from . import agent \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/agent.py b/level_1/hubspot_agent/hubspot/agent.py index 806f664..ac0e5da 100644 --- a/level_1/hubspot_agent/hubspot/agent.py +++ b/level_1/hubspot_agent/hubspot/agent.py @@ -1,14 +1,11 @@ from hubspot.hubspot_tool import create_lead from google.adk.agents import Agent +from hubspot.langfuse_config import get_langfuse_client -root_agent = Agent( - name="hubspot_agent", - model="gemini-2.0-flash", - description=( - "Agent to manage leads in HubSpot" - ), - instruction=( - """ +# Initialize Langfuse client +langfuse = get_langfuse_client() + +system_prompt = """ You are a HubSpot Agent designed to help administrators create and manage leads, and setup meetings with potential leads. @@ -46,6 +43,13 @@ Always think step-by-step about the most efficient and secure way to fulfill user requests. """ + +root_agent = Agent( + name="hubspot_agent", + model="gemini-2.0-flash", + description=( + "Agent to manage leads in HubSpot" ), + instruction=(system_prompt), tools=[create_lead] ) diff --git a/level_1/hubspot_agent/hubspot/config.py b/level_1/hubspot_agent/hubspot/config.py index 54aa8f7..a92947d 100644 --- a/level_1/hubspot_agent/hubspot/config.py +++ b/level_1/hubspot_agent/hubspot/config.py @@ -1,12 +1,22 @@ +import os +from dotenv import load_dotenv + class Config: def __init__(self): - pass + pass + + load_dotenv(dotenv_path='.env') + + gcp_project = os.getenv("GOOGLE_PROJECT") + db_name = os.getenv("POSTGRES_DB") # Database configuration database = { - "name": "database_name", # Database name + "name": db_name, # Database name "host": "localhost", # Database host - "port": 5432 # Database port + "port": 5432, # Database port + "db_user_key": "db_user", # Key name for database username in keyring + "db_password_key": "db_password" # Key name for database password in keyring } # HubSpot configuration @@ -14,6 +24,15 @@ def __init__(self): "base_url": "https://api.hubspot.com", "service_id": "hubspot_service", # Service ID for keyring "access_token_key": "access_token", # Key name for access token in keyring - "db_user_key": "db_user", # Key name for database username in keyring - "db_password_key": "db_password" # Key name for database password in keyring } + + # Email configuration + email = { + "imap_server": "your_imap_server.com", + "email_address": "your_email@example.com" + } + + # Google Cloud configuration + gcp = { + "gcp_project": gcp_project + } \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/hubspot_templates.py b/level_1/hubspot_agent/hubspot/hubspot_templates.py new file mode 100644 index 0000000..da99c99 --- /dev/null +++ b/level_1/hubspot_agent/hubspot/hubspot_templates.py @@ -0,0 +1,149 @@ +import datetime +import json + +from hubspot.hubspot_tool import HubSpotTool +from hubspot.langfuse_config import create_trace + + +class HubSpotTemplates: + """Class to store JSON templates for HubSpot API requests""" + + @staticmethod + def new_lead_template(email, firstname, lastname, phone, company, website): + return { + "properties": { + "email": email, + "firstname": firstname, + "lastname": lastname, + "phone": phone, + "company": company, + "website": website, + "lifecyclestage": "lead" + } + } + + @staticmethod + def update_lead_template(phone=None, state=None, lead_status=None): + properties = {"properties": {}} + + if phone: + properties["properties"]["phone"] = phone + if state: + properties["properties"]["state"] = state + if lead_status: + properties["properties"]["hs_lead_status"] = lead_status + + return properties + + @staticmethod + def meeting_template(start_time, end_time, lead_id, title, body): + return { + "properties": { + "hs_timestamp": start_time, + "hs_meeting_body": body, + "hs_meeting_end_time": end_time, + "hs_meeting_title": title, + }, + "associations": [ + { + "to": { + "id": lead_id + }, + "types": [ + { + "associationCategory": "HUBSPOT_DEFINED", + "associationTypeId": 191 # Contact to Meeting association type ID + } + ] + } + ], + "private": False + } + + +def main(): + + # Create a trace for the entire agent flow + with create_trace("hubspot_agent_flow", metadata={"source": "manual_run"}) as trace: + + # Initialize HubSpot and PostgreSQL clients + hs_tool = HubSpotTool() + lead_data = HubSpotTemplates.new_lead_template( + email="new.lead.private.app@example.com", + firstname="App", + lastname="Lead", + phone="111-222-3333", + company="App Integrations Inc.", + website="https://www.appintegrations.com" + ) + + # Create a span for new lead creation + with trace.span( + name="create_lead_process", + input=lead_data, + metadata={"step": "initial_creation"} + ) as create_span: + new_lead_id = hs_tool.create_lead(lead_data) + create_span.update(output=new_lead_id, metadata={ + "step": "initial_creation", "status": "success" if new_lead_id else "failed"} + ) + + if new_lead_id: + + # Example 2: Updating the lead + print(f"\n--- Modifying Lead with ID: {new_lead_id} using Private App Token ---") + updated_properties = HubSpotTemplates.update_lead_template( + phone="444-555-6666", + state="NY", + lead_status="Open" + ) + + # Create a span for updating the lead + with trace.span( + name="update_lead_process", + input=updated_properties, + metadata={"step": "update"} + ) as update_span: + modified_lead_response = hs_tool.update_lead( + lead_id=new_lead_id, + properties=updated_properties + ) + update_span.update(output=modified_lead_response, metadata={ + "step": "update", "status": "success" if modified_lead_response else "failed"} + ) + + if modified_lead_response: + print("Lead modified successfully using Private App Token:") + print(json.dumps(modified_lead_response, indent=2)) + else: + print("Failed to modify lead using Private App Token.") + + # Example 3: Creating a meeting + print(f"\n--- Creating a Meeting associated with Lead ID: {new_lead_id} using Private App Token ---") + + now = datetime.datetime.now(datetime.timezone.utc) + start_time = now.isoformat() + end_time = (now + datetime.timedelta(minutes=30)).isoformat() # Meeting for 30 mins + + meeting_data = HubSpotTemplates.meeting_template( + start_time=start_time, + end_time=end_time, + lead_id=new_lead_id, + title="Follow-up Call (App)", + body="Follow-up discussion via Private App." + ) + + created_meeting_response = hs_tool.create_meeting(meeting_data=meeting_data) + + if created_meeting_response: + print("Meeting created and associated successfully using Private App Token:") + print(json.dumps(created_meeting_response, indent=2)) + + # Store meeting in database + meeting_id = created_meeting_response.get('id') + + else: + print("Failed to create meeting using Private App Token.") + else: + print("\nSkipping lead modification and meeting creation as lead creation failed.") + diff --git a/level_1/hubspot_agent/hubspot/hubspot_tool.py b/level_1/hubspot_agent/hubspot/hubspot_tool.py index 4688058..93ff5ce 100644 --- a/level_1/hubspot_agent/hubspot/hubspot_tool.py +++ b/level_1/hubspot_agent/hubspot/hubspot_tool.py @@ -4,7 +4,7 @@ import keyring from hubspot.config import Config from hubspot.postgres_client import PostgresClient - +from langfuse.decorators import langfuse_context, observe class HubSpotTool(): """Class to interact with the HubSpot API""" @@ -15,8 +15,8 @@ def __init__(self, base_url=None): # Get configuration from config.py service_id = Config.hubspot["service_id"] access_token_key = Config.hubspot["access_token_key"] - db_user_key = Config.hubspot["db_user_key"] - db_password_key = Config.hubspot["db_password_key"] + db_user_key = Config.database["db_user_key"] + db_password_key = Config.database["db_password_key"] # Get HubSpot access token and database credentials from keyring self.hubspot_access_token = keyring.get_password(service_id, access_token_key) @@ -44,6 +44,14 @@ def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): """Helper function to make API calls using Bearer Token""" access_token = self.hubspot_access_token print("Calling ", endpoint, " method ", method, " with payload \n", data) + span = langfuse_context.get_current_observation() + if span: + span.log(key="api_call_details", value={ + "message": "Calling API", + "endpoint": endpoint, + "method": method, + "payload": data + }) url = f"{self.BASE_URL}{endpoint}" headers = { @@ -52,10 +60,21 @@ def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): } print(f"Making {method} request to {url}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="http_request", value={ + "message": f"Making {method} request to {url}" + }) if params: print(f"Query parameters: {params}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="query_parameters", value=params) if data: print(f"Request body: {json.dumps(data, indent=2)}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="request_body", value=json.dumps(data, indent=2)) try: if method == 'POST': @@ -72,22 +91,40 @@ def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx) print(f"Response Status Code: {response.status_code}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="response_status", value=response.status_code) # Attempt to parse JSON response if content exists if response.content: try: return response.json() except json.JSONDecodeError: print("Warning: Response was not JSON.") + span = langfuse_context.get_current_observation() + if span: + span.log(key="json_decode_warning", value="Response was not JSON") return response.text # Return plain text if not JSON else: print("Info: Response body is empty.") + span = langfuse_context.get_current_observation() + if span: + span.log(key="empty_response_body", value="Response body is empty") return None # No content in response except requests.exceptions.RequestException as e: print(f"API Request failed: {e}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="api_request_failed", value=str(e), level="ERROR") if hasattr(e, 'response') and e.response is not None: print(f"Status Code: {e.response.status_code}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="failed_response_status", value=e.response.status_code, level="ERROR") print(f"Response Body: {e.response.text}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="failed_response_body", value=e.response.text, level="ERROR") return None def delete_lead(self, lead_id): @@ -99,16 +136,40 @@ def create_lead(self, lead_data: dict): # Example 1: Creating a new lead print("\n--- Creating a New Lead (Contact) using Private App Token ---") + span = langfuse_context.get_current_observation() + if span: + span.log(key="create_lead_start", value="Creating a New Lead (Contact) using Private App Token") print(f"Lead data received: {json.dumps(lead_data, indent=2)}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="lead_data_received", value=json.dumps(lead_data, indent=2)) """Create a new lead in HubSpot""" create_lead_endpoint = "/crm/v3/objects/contacts" - created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=lead_data) + + # Add Langfuse span for the API call + span = langfuse_context.get_current_observation() + if span: + with span.span(name="hubspot-api-create-lead", input=lead_data, metadata={"endpoint": create_lead_endpoint, "method": "POST"}) as api_span: + created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=lead_data) + api_span.update(output=created_lead_response) + else: + # If there's no active span, just call the API without tracing + created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=lead_data) if created_lead_response: print("Lead created successfully using Private App Token:") + span = langfuse_context.get_current_observation() + if span: + span.log(key="create_lead_success", value="Lead created successfully using Private App Token") print(json.dumps(created_lead_response, indent=2)) + span = langfuse_context.get_current_observation() + if span: + span.log(key="created_lead_response", value=json.dumps(created_lead_response, indent=2)) new_lead_id = created_lead_response.get('id') print(f"Newly created lead ID: {new_lead_id}") + span = langfuse_context.get_current_observation() + if span: + span.log(key="new_lead_id", value=new_lead_id) try: # Store the lead in PostgreSQL if created successfully @@ -117,18 +178,43 @@ def create_lead(self, lead_data: dict): return new_lead_id else: print("Failed to create lead using Private App Token.") + span = langfuse_context.get_current_observation() + if span: + span.log(key="create_lead_failed", value="Failed to create lead using Private App Token", level="ERROR") return None def update_lead(self, properties: json): lead_id = properties.get('lead_id') """Update an existing lead in HubSpot""" modify_lead_endpoint = f"/crm/v3/objects/contacts/{lead_id}" - return self.call_hubspot_api_oauth('PATCH', modify_lead_endpoint, data=properties) + # Get current span for logging within this method + span = langfuse_context.get_current_observation() + if span: + span.log(key="update_lead_start", value={ + "message": "Updating Lead", + "lead_id": lead_id, + "properties": properties + }) + response = self.call_hubspot_api_oauth('PATCH', modify_lead_endpoint, data=properties) + span = langfuse_context.get_current_observation() + if span: + span.log(key="update_lead_response", value=response) + return response - def create_meeting(self, meeting_data: dict): + def create_meeting(self, meeting_data: json): """Create a new meeting in HubSpot""" create_meeting_endpoint = "/crm/v3/objects/meetings" + # Get current span for logging within this method + span = langfuse_context.get_current_observation() + if span: + span.log(key="create_meeting_start", value={ + "message": "Creating Meeting", + "meeting_data": meeting_data + }) response = self.call_hubspot_api_oauth('POST', create_meeting_endpoint, data=meeting_data) + span = langfuse_context.get_current_observation() + if span: + span.log(key="create_meeting_response", value=response) meeting_id = response.get('meeting_id') lead_id = response.get('lead_id') if meeting_id: @@ -139,13 +225,20 @@ def create_meeting(self, meeting_data: dict): start_time=meeting_data["properties"]["start_time"], end_time=meeting_data["properties"]["end_time"] ) + span = langfuse_context.get_current_observation() + if span: + span.log(key="meeting_stored_in_db", value={ + "meeting_id": meeting_id, + "lead_id": lead_id, + "db_id": db_meeting_id + }) return response - hstool = HubSpotTool() - - -def create_lead(json_payload: dict): +def create_lead(json_payload:dict): print('Agent called "create_lead()"') - return hstool.create_lead(json_payload) + span = langfuse_context.get_current_observation() + if span: + span.log(key="agent_create_lead_called", value="Agent called create_lead()") + return hstool.create_lead(json_payload) \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/langfuse_config.py b/level_1/hubspot_agent/hubspot/langfuse_config.py new file mode 100644 index 0000000..c28823c --- /dev/null +++ b/level_1/hubspot_agent/hubspot/langfuse_config.py @@ -0,0 +1,12 @@ +from langfuse import Langfuse +import os + +# Initialize Langfuse client for local instance +langfuse = Langfuse( + public_key="pk-lf-1234567890", # This can be any string for local development + secret_key="sk-lf-1234567890", # This can be any string for local development + host="http://localhost:3000" # Local Langfuse instance +) + +def get_langfuse_client(): + return langfuse \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/postgres_client.py b/level_1/hubspot_agent/hubspot/postgres_client.py index e18900b..fd2a4c5 100644 --- a/level_1/hubspot_agent/hubspot/postgres_client.py +++ b/level_1/hubspot_agent/hubspot/postgres_client.py @@ -57,7 +57,7 @@ def store_lead(self, hubspot_id, lead_data): """Store a new lead in the database""" conn = self.conn_pool.getconn() email = lead_data['properties']['email'] - firstname = lead_data['properties'].get('firstname') # Use .get for optional fields + firstname = lead_data['properties'].get('firstname') # Use .get for optional fields lastname = lead_data['properties'].get('lastname') phone = lead_data['properties'].get('phone') try: @@ -122,4 +122,4 @@ def get_lead_by_hubspot_id(self, hubspot_id): def close(self): """Close the connection pool""" if self.conn_pool: - self.conn_pool.closeall() + self.conn_pool.closeall() \ No newline at end of file diff --git a/level_1/hubspot_agent/requirements.txt b/level_1/hubspot_agent/requirements.txt index 35343c8..b4c1e3f 100644 --- a/level_1/hubspot_agent/requirements.txt +++ b/level_1/hubspot_agent/requirements.txt @@ -1,8 +1,7 @@ google-adk +google.adk google-search-results keyring psycopg2 -requests -json -datetime -getpass \ No newline at end of file +langfuse>=2.0.0 +pydantic[email] \ No newline at end of file From 919935d4069082e441b49771e4b838edf157b79b Mon Sep 17 00:00:00 2001 From: rcmohan Date: Tue, 20 May 2025 07:14:42 -0400 Subject: [PATCH 04/11] Reading emails --- level_1/hubspot_agent/hubspot/.env | 8 +++- level_1/hubspot_agent/hubspot/email_reader.py | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 level_1/hubspot_agent/hubspot/email_reader.py diff --git a/level_1/hubspot_agent/hubspot/.env b/level_1/hubspot_agent/hubspot/.env index b17d7c2..bc56ec3 100644 --- a/level_1/hubspot_agent/hubspot/.env +++ b/level_1/hubspot_agent/hubspot/.env @@ -1,2 +1,6 @@ -GOOGLE_GENAI_USE_VERTEXAI=FALSE -GOOGLE_API_KEY=AIzaS... \ No newline at end of file +GOOGLE_GENAI_USE_VERTEXAI=TRUE +GOOGLE_API_KEY=AIzaS... +POSTGRES_DB= +GOOGLE_PROJECT= +CONFIGURED_EMAIL_ID= +IMAP_SERVER= \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/email_reader.py b/level_1/hubspot_agent/hubspot/email_reader.py new file mode 100644 index 0000000..918bbce --- /dev/null +++ b/level_1/hubspot_agent/hubspot/email_reader.py @@ -0,0 +1,43 @@ +import imaplib +import email +import os +from config import Config +import keyring + +# Assuming you have a function to process the email content with your agent +# from hubspot.agent import process_email_with_agent + +# Email server details +IMAP_SERVER = Config.email["imap_server"] +EMAIL_ADDRESS = Config.email["email_address"] +EMAIL_PASSWORD = keyring.get_password(Config.hubspot["service_id"], Config.email["email_password"]) + +def fetch_emails(): + """Fetches unread emails from the configured IMAP server.""" + try: + mail = imaplib.IMAP4_SSL(IMAP_SERVER) + mail.login(EMAIL_ADDRESS, EMAIL_PASSWORD) + mail.select('inbox') # Select the inbox + + # Search for unread emails + status, email_ids = mail.search(None, '(UNSEEN)') + if status == 'OK': + for email_id in email_ids[0].split(): + status, msg_data = mail.fetch(email_id, '(RFC822)') + if status == 'OK': + raw_email = msg_data[0][1] + msg = email.message_from_bytes(raw_email) + + # Process the email message + # You would extract relevant parts (body, etc.) and + # pass it to your agent's processing function + print(f"Processing email from: {msg['from']}") + # process_email_with_agent(msg) # Call your agent processing function + + mail.logout() + except Exception as e: + print(f"Error fetching emails: {e}") + +if __name__ == "__main__": + # Example usage: Call fetch_emails() to retrieve and process emails + fetch_emails() From e45b3222eb94f27e7dab6787ee7511c74e0478bc Mon Sep 17 00:00:00 2001 From: rcmohan Date: Wed, 21 May 2025 07:48:28 -0400 Subject: [PATCH 05/11] Fixed the email reader --- level_1/{hubspot_agent/hubspot => }/.env | 0 level_1/hubspot_agent/adk_agent.py | 88 +++++--- level_1/hubspot_agent/adk_ui.py | 200 ++++++++++++++++++ level_1/hubspot_agent/docker-compose.yml | 27 +++ level_1/hubspot_agent/hubspot/config.py | 2 +- level_1/hubspot_agent/hubspot/email_reader.py | 62 +++++- level_1/hubspot_agent/requirements.txt | 9 +- 7 files changed, 346 insertions(+), 42 deletions(-) rename level_1/{hubspot_agent/hubspot => }/.env (100%) create mode 100644 level_1/hubspot_agent/adk_ui.py create mode 100644 level_1/hubspot_agent/docker-compose.yml diff --git a/level_1/hubspot_agent/hubspot/.env b/level_1/.env similarity index 100% rename from level_1/hubspot_agent/hubspot/.env rename to level_1/.env diff --git a/level_1/hubspot_agent/adk_agent.py b/level_1/hubspot_agent/adk_agent.py index 1b47211..9f8959b 100644 --- a/level_1/hubspot_agent/adk_agent.py +++ b/level_1/hubspot_agent/adk_agent.py @@ -36,6 +36,8 @@ class EmailContent(BaseModel): body: str = Field(..., example="Hello team, here's an update...", description="Main content/body of the email.") received_at: Optional[datetime.datetime] = Field(None, example="2024-05-18T14:30:00Z", description="Timestamp when the email was received.") message_id: Optional[str] = Field(None, example="", description="Unique identifier for the email message.") + session_id: Optional[str] = Field(None, example="", description="Unique identifier for the email session.") + user_id: str = Field(..., example="user123", description="Unique identifier for the user.") class Config: # This allows FastAPI to show example values in the documentation @@ -46,14 +48,15 @@ class Config: "subject": "Inquiry about product X", "body": "Dear Support Team,\n\nI have a question regarding product X. Could you please provide more details on its features?\n\nThanks,\nJane", "received_at": "2024-05-18T10:00:00Z", - "message_id": "" + "message_id": "", + "session_id": "", + "user_id": "user123" } } - -# Initialize ADK App with your root agent -# Ensure root_agent is correctly defined and imported + try: adk_app = AdkApp(agent=root_agent) + print("ADK Agent started!") except Exception as e: print(f"Error initializing AdkApp with root_agent: {e}") # Handle this critical error appropriately, as the app won't function @@ -61,40 +64,65 @@ class Config: -@app.post("/chat") -async def chat_with_agent(query: str): - - session = adk_app.create_session() - response_events = [] - async for event in session.execute(input={"message": query}): - response_events.append(event) - if event.name == "agent:llm:final_response": - return {"response": event.data.get("text")} - return {"response_events": response_events} # Fallback - # --- New Endpoint for Processing Email Content --- @app.post("/process-email", tags=["Agent Interaction"]) async def process_email_with_agent(email_data: EmailContent): + print('Processing: ', email_data) if not adk_app: raise HTTPException(status_code=500, detail="ADK Application not initialized.") try: - session = adk_app.create_session() # Consider if session management is needed per email or per sender - response_events = [] - final_text_response = None - - async for event in session.execute(input={"message": system_prompt}): - response_events.append(event.model_dump_json()) # Storing JSON serializable event data - if event.name == "agent:llm:final_response": - final_text_response = event.data.get("text") - return { - "status": "Email processed", - "agent_response": final_text_response, - "original_subject": email_data.subject, - "original_sender": email_data.sender, - "session_id": session.session_id - } + # Use provided session_id if available, otherwise create new session + session = adk_app.create_session(user_id=email_data.sender, session_id=email_data.session_id) + print(f"Sending to agent (session {session}): {email_data.body}") + + + + full_response_text = "" + final_response_event_data = None + + async for event in adk_app.stream_query(message=email_data.body, user_id=email_data.sender, session=session): + print(f"Received event: {event.name}, Data: {event.data}") # Log all events for debugging + + if event.name == "agent:llm:chunk": # Streaming text from the LLM + if "text" in event.data: + chunk_text = event.data["text"] + print(chunk_text, end="", flush=True) # Print chunks as they arrive + full_response_text += chunk_text + elif event.name == "agent:llm:final_response": # A consolidated final response from the LLM + if "text" in event.data: + # If we haven't built up text from chunks, use this + if not full_response_text: + full_response_text = event.data["text"] + final_response_event_data = event.data # Store the whole final response data + print(f"\nFinal response event data: {final_response_event_data}") # Newline after chunks + break # Often, the final_response event is the last one you need for the text output + elif event.name == "agent:error": + print(f"\nAgent error: {event.data.get('message')}") + # Handle error appropriately + return f"Agent error: {event.data.get('message')}" + # You can handle other event types here (e.g., tool calls) if needed + + + # session = adk_app.create_session( + # user_id=email_data.user_id, + # session_id=email_data.session_id + # ) if hasattr(email_data, 'session_id') and email_data.session_id else adk_app.create_session(user_id=email_data.user_id) + # response_events = [] + # final_text_response = None + + # async for event in session.chat(input={"message": system_prompt}): + # response_events.append(event.model_dump_json()) # Storing JSON serializable event data + # if event.name == "agent:llm:final_response": + # final_text_response = event.data.get("text") + # return { + # "status": "Email processed", + # "agent_response": final_text_response, + # "original_subject": email_data.subject, + # "original_sender": email_data.sender, + # "session_id": session.session_id + # } # Fallback if no 'agent:llm:final_response' event with text was found # This logic mirrors the /chat endpoint's fallback diff --git a/level_1/hubspot_agent/adk_ui.py b/level_1/hubspot_agent/adk_ui.py new file mode 100644 index 0000000..ecbc950 --- /dev/null +++ b/level_1/hubspot_agent/adk_ui.py @@ -0,0 +1,200 @@ +import streamlit as st +import requests +import json +from datetime import datetime +import time +import sys +from hubspot.email_reader import fetch_emails +import email.utils +import dateutil.parser + +# Print debug information +print("Starting Streamlit app...") + + +# Constants +API_BASE_URL = "http://localhost:8000" # Update this if your FastAPI server runs on a different port + + +class ADKAgentUI: + def __init__(self): + self.session_id = None + self.is_running = False + self.emails = fetch_emails(search_param='(ALL)') + self.user_id = "default_user" # You might want to make this configurable + + def get_emails(self): + return self.emails + + def check_agent(self): + try: + response = requests.get(f"{API_BASE_URL}/") + print("Is agent running?", response) + if response.status_code == 200: + self.is_running = True + return True, "Agent is running!" + return False, "Agent is not running" + except Exception as e: + return False, f"Agent is not running" + + def send_message(self, message): + if not self.is_running: + return False, "Agent is not running" + + try: + response = requests.post( + f"{API_BASE_URL}/chat", + json={"query": message} + ) + if response.status_code == 200: + return True, response.json()["response"] + return False, f"Error: {response.text}" + except Exception as e: + return False, f"Error sending message: {str(e)}" + + def send_email(self, sender, recipient, subject, body, received_at, message_id): + if not self.is_running and self.check_agent(): + return False, "Agent is not running" + + try: + # Parse the received_at string into a datetime object if it's a string + if isinstance(received_at, str): + try: + # Try parsing RFC 2822 format first + parsed_date = email.utils.parsedate_to_datetime(received_at) + except: + # Fallback to dateutil parser for other formats + parsed_date = dateutil.parser.parse(received_at) + received_at = parsed_date.isoformat() + + # Format email data according to EmailContent model + email_data = { + "sender": sender, + "recipient": recipient, + "subject": subject, + "body": body, + "received_at": received_at, + "message_id": message_id, + "session_id": self.session_id, # Include the session ID + "user_id": self.user_id # Include the user ID + } + + print("Sending email data:", email_data) # Debug print + + response = requests.post( + f"{API_BASE_URL}/process-email", + json=email_data + ) + + if response.status_code == 200: + response_data = response.json() + # Update session ID from response if provided + if 'session_id' in response_data: + self.session_id = response_data['session_id'] + return True, response_data + elif response.status_code == 422: + error_detail = response.json().get('detail', 'Validation error') + return False, f"Validation error: {error_detail}" + else: + return False, f"Error: {response.text}" + except Exception as e: + return False, f"Error processing email: {str(e)}" + + + +try: + # Initialize session state + if 'agent' not in st.session_state: + st.session_state.agent = ADKAgentUI() + if 'selected_emails' not in st.session_state: + st.session_state.selected_emails = st.session_state.agent.get_emails() + +except Exception as e: + st.error(f"An error occurred: {str(e)}") + st.error("Please check if the FastAPI server is running at " + API_BASE_URL) + + + +st.set_page_config( + page_title="ADK Agent Control Panel", + page_icon="🤖", + layout="wide" +) + + +# Agent Status +st.header("Agent Controls") +status_color = "green" if st.session_state.agent.is_running else "red" +st.markdown(f"Status: {'Running' if st.session_state.agent.is_running else 'Stopped'}", unsafe_allow_html=True) + + + + + +# Email Processing +st.header("Email Processing") +with st.expander("Fetched Emails", expanded=True): + if st.session_state.selected_emails: + + # Create a table with buttons + st.markdown(""" + + """, unsafe_allow_html=True) + + # Table header + cols = st.columns([0.05, 0.25, 0.55, 0.15]) + cols[0].write("Action") + cols[1].write("Sender") + cols[2].write("Subject") + cols[3].write("Date") + + # Table rows + for idx, email in enumerate(st.session_state.selected_emails): + cols = st.columns([0.05, 0.25, 0.55, 0.15]) + + # Button column + msg_id = email.get('message_id', '') + subject = email.get('subject', '') + sender = email.get('sender', '') + body = email.get('body', '') + recipient = email.get('recipient', '') + sent_at = email.get('received_at', datetime.now()) + + + + if cols[0].button("🤖", key=msg_id): + status, resp = st.session_state.agent.send_email(sender=sender, subject=subject, recipient=recipient, body=body, received_at=sent_at, message_id=msg_id) + + # Show status and response in sidebar + with st.sidebar: + if status: + st.success("Message processed successfully!") + st.json(resp) + else: + st.error("Failed to process message") + st.error(resp) + + # Data columns + cols[1].write(sender) + cols[2].write(subject) + cols[3].write(email.get('received_at', '')) + + # Show selected emails count + if st.session_state.selected_emails: + st.write(f"Selected emails: {len(st.session_state.selected_emails)}") + else: + st.info("No emails found.") + +try: + # Test if we can make a request to the API + response = requests.get(f"{API_BASE_URL}/") + st.success(f"API Connection Test: {response.status_code}") +except Exception as e: + st.error(f"API Connection Error: {str(e)}") + diff --git a/level_1/hubspot_agent/docker-compose.yml b/level_1/hubspot_agent/docker-compose.yml new file mode 100644 index 0000000..0e579e0 --- /dev/null +++ b/level_1/hubspot_agent/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.8' +services: + postgres: + image: postgres:15 + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + langfuse: + image: langfuse/langfuse:latest + ports: + - "3000:3000" + environment: + - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + - NEXTAUTH_URL=http://localhost:3000 + - NEXTAUTH_SECRET=your-secret-key-here + - AUTH_DISABLED=true # Disable authentication for local development + depends_on: + - postgres + +volumes: + postgres_data: \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/config.py b/level_1/hubspot_agent/hubspot/config.py index a92947d..bb1f448 100644 --- a/level_1/hubspot_agent/hubspot/config.py +++ b/level_1/hubspot_agent/hubspot/config.py @@ -5,7 +5,7 @@ class Config: def __init__(self): pass - load_dotenv(dotenv_path='.env') + load_dotenv(dotenv_path='../../.env') gcp_project = os.getenv("GOOGLE_PROJECT") db_name = os.getenv("POSTGRES_DB") diff --git a/level_1/hubspot_agent/hubspot/email_reader.py b/level_1/hubspot_agent/hubspot/email_reader.py index 918bbce..0168804 100644 --- a/level_1/hubspot_agent/hubspot/email_reader.py +++ b/level_1/hubspot_agent/hubspot/email_reader.py @@ -1,7 +1,7 @@ import imaplib import email import os -from config import Config +from hubspot.config import Config import keyring # Assuming you have a function to process the email content with your agent @@ -12,32 +12,74 @@ EMAIL_ADDRESS = Config.email["email_address"] EMAIL_PASSWORD = keyring.get_password(Config.hubspot["service_id"], Config.email["email_password"]) -def fetch_emails(): +def get_email_body(email_message: email.message.Message) -> str: + """Extract email body content""" + body = "" + if email_message.is_multipart(): + for part in email_message.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + + if "attachment" not in content_disposition: + if content_type == "text/plain": + try: + body += part.get_payload(decode=True).decode() + except: + body += part.get_payload() + else: + try: + body = email_message.get_payload(decode=True).decode() + except: + body = email_message.get_payload() + return body + + +def fetch_emails(count = 10, search_param='(UNSEEN)'): """Fetches unread emails from the configured IMAP server.""" try: + + ret = [] + mail = imaplib.IMAP4_SSL(IMAP_SERVER) mail.login(EMAIL_ADDRESS, EMAIL_PASSWORD) mail.select('inbox') # Select the inbox + # Search for unread emails - status, email_ids = mail.search(None, '(UNSEEN)') + status, messages = mail.search(None, search_param) if status == 'OK': - for email_id in email_ids[0].split(): + email_ids = messages[0].split() + + if count: + email_ids = email_ids[-count:] + for email_id in email_ids: status, msg_data = mail.fetch(email_id, '(RFC822)') if status == 'OK': raw_email = msg_data[0][1] msg = email.message_from_bytes(raw_email) + - # Process the email message - # You would extract relevant parts (body, etc.) and - # pass it to your agent's processing function print(f"Processing email from: {msg['from']}") - # process_email_with_agent(msg) # Call your agent processing function + msg_data = { + "sender": msg['From'], + "recipient": msg['To'], + "subject": msg['Subject'], + "body": get_email_body(msg), + "received_at": msg['Date'], + "message_id": msg['Message-ID'] + } + ret.append(msg_data) - mail.logout() + return ret except Exception as e: print(f"Error fetching emails: {e}") + return [] + finally: + if mail: + mail.logout() + if __name__ == "__main__": # Example usage: Call fetch_emails() to retrieve and process emails - fetch_emails() + ret = fetch_emails(1, '(ALL)') + print(ret) diff --git a/level_1/hubspot_agent/requirements.txt b/level_1/hubspot_agent/requirements.txt index b4c1e3f..f13e5d2 100644 --- a/level_1/hubspot_agent/requirements.txt +++ b/level_1/hubspot_agent/requirements.txt @@ -1,6 +1,13 @@ +streamlit>=1.32.0 +requests>=2.31.0 +python-dotenv>=1.0.0 +fastapi>=0.109.0 +uvicorn>=0.27.0 +pydantic>=2.6.0 +email-validator>=2.1.0 +imaplib2>=3.6 google-adk google.adk -google-search-results keyring psycopg2 langfuse>=2.0.0 From a2e403631e2b82b087be0126051dd07e3eefaa36 Mon Sep 17 00:00:00 2001 From: rcmohan Date: Wed, 21 May 2025 20:12:31 -0400 Subject: [PATCH 06/11] Fixed the client -- added session, cleaned up json --- level_1/hubspot_agent/adk_agent.py | 30 --------------------- level_1/hubspot_agent/adk_ui.py | 36 +++++++++++++++++++++----- level_1/hubspot_agent/hubspot/agent.py | 2 +- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/level_1/hubspot_agent/adk_agent.py b/level_1/hubspot_agent/adk_agent.py index 9f8959b..8ab840d 100644 --- a/level_1/hubspot_agent/adk_agent.py +++ b/level_1/hubspot_agent/adk_agent.py @@ -105,36 +105,6 @@ async def process_email_with_agent(email_data: EmailContent): # You can handle other event types here (e.g., tool calls) if needed - # session = adk_app.create_session( - # user_id=email_data.user_id, - # session_id=email_data.session_id - # ) if hasattr(email_data, 'session_id') and email_data.session_id else adk_app.create_session(user_id=email_data.user_id) - # response_events = [] - # final_text_response = None - - # async for event in session.chat(input={"message": system_prompt}): - # response_events.append(event.model_dump_json()) # Storing JSON serializable event data - # if event.name == "agent:llm:final_response": - # final_text_response = event.data.get("text") - # return { - # "status": "Email processed", - # "agent_response": final_text_response, - # "original_subject": email_data.subject, - # "original_sender": email_data.sender, - # "session_id": session.session_id - # } - - # Fallback if no 'agent:llm:final_response' event with text was found - # This logic mirrors the /chat endpoint's fallback - if final_text_response: - return {"status": "Email processed", "agent_response": final_text_response, "session_id": session.session_id} - - return { - "status": "Email processed, but no direct text response from agent.", - "agent_response_summary": "Agent processed the email content.", - "all_events": response_events, # Be cautious with returning all events in production - "session_id": session.session_id - } except Exception as e: # Log the exception for debugging diff --git a/level_1/hubspot_agent/adk_ui.py b/level_1/hubspot_agent/adk_ui.py index ecbc950..25957c5 100644 --- a/level_1/hubspot_agent/adk_ui.py +++ b/level_1/hubspot_agent/adk_ui.py @@ -23,12 +23,13 @@ def __init__(self): self.emails = fetch_emails(search_param='(ALL)') self.user_id = "default_user" # You might want to make this configurable + def get_emails(self): return self.emails def check_agent(self): try: - response = requests.get(f"{API_BASE_URL}/") + response = requests.get(f"{API_BASE_URL}/docs") print("Is agent running?", response) if response.status_code == 200: self.is_running = True @@ -51,6 +52,8 @@ def send_message(self, message): return False, f"Error: {response.text}" except Exception as e: return False, f"Error sending message: {str(e)}" + + def send_email(self, sender, recipient, subject, body, received_at, message_id): if not self.is_running and self.check_agent(): @@ -67,8 +70,7 @@ def send_email(self, sender, recipient, subject, body, received_at, message_id): parsed_date = dateutil.parser.parse(received_at) received_at = parsed_date.isoformat() - # Format email data according to EmailContent model - email_data = { + payload_json = { "sender": sender, "recipient": recipient, "subject": subject, @@ -78,13 +80,33 @@ def send_email(self, sender, recipient, subject, body, received_at, message_id): "session_id": self.session_id, # Include the session ID "user_id": self.user_id # Include the user ID } + + # Format email data according to EmailContent model + email_data = { + "appName": "hubspot", + "userId": self.user_id, + # "sessionId": self.session_id, + "newMessage": { + "parts": [{ + "text": json.dumps(payload_json) + }] + } + } print("Sending email data:", email_data) # Debug print + + + if not self.session_id: + self.session_id = f"s-{self.user_id}" + session_url = f"{API_BASE_URL}/apps/hubspot/users/{self.user_id}/sessions/{self.session_id}" + response = requests.post(session_url) + + response = requests.post( f"{API_BASE_URL}/run", ) - response = requests.post( - f"{API_BASE_URL}/process-email", - json=email_data - ) + # response = requests.post( + # f"{API_BASE_URL}/process-email", + # json=email_data + # ) if response.status_code == 200: response_data = response.json() diff --git a/level_1/hubspot_agent/hubspot/agent.py b/level_1/hubspot_agent/hubspot/agent.py index ac0e5da..619bc0f 100644 --- a/level_1/hubspot_agent/hubspot/agent.py +++ b/level_1/hubspot_agent/hubspot/agent.py @@ -5,7 +5,7 @@ # Initialize Langfuse client langfuse = get_langfuse_client() -system_prompt = """ +system_prompt = """ You are a HubSpot Agent designed to help administrators create and manage leads, and setup meetings with potential leads. From 1aab98e7bb05029102805fb0602f0fc238c3559d Mon Sep 17 00:00:00 2001 From: rcmohan Date: Thu, 22 May 2025 21:15:32 -0400 Subject: [PATCH 07/11] Fixes to tool --- level_1/hubspot_agent/hubspot/hubspot_tool.py | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/level_1/hubspot_agent/hubspot/hubspot_tool.py b/level_1/hubspot_agent/hubspot/hubspot_tool.py index 93ff5ce..03a3c40 100644 --- a/level_1/hubspot_agent/hubspot/hubspot_tool.py +++ b/level_1/hubspot_agent/hubspot/hubspot_tool.py @@ -5,6 +5,10 @@ from hubspot.config import Config from hubspot.postgres_client import PostgresClient from langfuse.decorators import langfuse_context, observe +from google.adk.agents import Agent +from hubspot.langfuse_config import get_langfuse_client +from google.adk.models.lite_llm import LiteLlm +from google.adk.tools import FunctionTool class HubSpotTool(): """Class to interact with the HubSpot API""" @@ -133,7 +137,6 @@ def delete_lead(self, lead_id): return self.call_hubspot_api_oauth(method='DELETE', endpoint=delete_lead_endpoint) def create_lead(self, lead_data: dict): - # Example 1: Creating a new lead print("\n--- Creating a New Lead (Contact) using Private App Token ---") span = langfuse_context.get_current_observation() @@ -236,9 +239,46 @@ def create_meeting(self, meeting_data: json): return response hstool = HubSpotTool() -def create_lead(json_payload:dict): + + +def create_lead(json_payload:dict): + """Create a new lead (contact) in HubSpot and store it in the local database. + + This function creates a new contact in HubSpot using the HubSpot API and then stores + the lead information in a local PostgreSQL database for tracking purposes. + + Args: + lead_data (dict): A dictionary containing the lead's properties. The dictionary should + follow HubSpot's contact properties format. Example: + { + "properties": { + "email": "contact@example.com", + "firstname": "John", + "lastname": "Doe", + "phone": "123-456-7890", + "company": "Example Corp", + "website": "https://example.com", + "lifecyclestage": "lead" + } + } + + Returns: + str: The HubSpot ID of the newly created lead if successful, None if the creation fails. + + Raises: + requests.exceptions.RequestException: If the API call to HubSpot fails. + Exception: If there's an error storing the lead in the local database. + + Note: + - The function uses the HubSpot Private App Token for authentication + - All API calls and database operations are logged using Langfuse for observability + - The lead is stored in both HubSpot and a local PostgreSQL database + """ print('Agent called "create_lead()"') span = langfuse_context.get_current_observation() if span: span.log(key="agent_create_lead_called", value="Agent called create_lead()") - return hstool.create_lead(json_payload) \ No newline at end of file + return hstool.create_lead(json_payload) + +create_lead_tool = FunctionTool(func=create_lead) +# , name="create_lead", description="Create a new lead (contact) in HubSpot and store it in the local database.") \ No newline at end of file From bf66cf1c7563f9f87002a40bf4fa23cd95cf0d39 Mon Sep 17 00:00:00 2001 From: rcmohan Date: Fri, 23 May 2025 07:42:08 -0400 Subject: [PATCH 08/11] Fixes to tool --- level_1/hubspot_agent/hubspot/agent.py | 76 ++++++++++++-------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/level_1/hubspot_agent/hubspot/agent.py b/level_1/hubspot_agent/hubspot/agent.py index 619bc0f..b4c1bf0 100644 --- a/level_1/hubspot_agent/hubspot/agent.py +++ b/level_1/hubspot_agent/hubspot/agent.py @@ -1,48 +1,44 @@ -from hubspot.hubspot_tool import create_lead +from hubspot_tool import create_lead from google.adk.agents import Agent -from hubspot.langfuse_config import get_langfuse_client +from langfuse_config import get_langfuse_client # Initialize Langfuse client langfuse = get_langfuse_client() -system_prompt = """ - You are a HubSpot Agent designed to help administrators create and manage leads, and setup meetings with - potential leads. - - Your capabilities: - 1. Read emails and extract data about the lead - 2. Call Hubspot APIs using HubSpotTool - - When you read an email: - 1. Identify the fields that match the list in the {contact.csv} file - 2. Create a json as in the following example: - { - "properties": { - "email": "new.lead.private.app@example.com", - "firstname": "App", - "lastname": "Lead", - "phone": "111-222-3333", - "company": "App Integrations Inc.", - "website": "https://www.appintegrations.com", - "lifecyclestage": "lead" - } - } - 3. Invoke the create_lead tool or modify_lead tool with the appropriate json. - - TOOLS AVAILABLE: - - create_lead: - - Description: Creates a new lead in HubSpot. Accepts a single parameter `json_payload` which must be a JSON object containing the lead's properties. - - Example workflow: - 1. Receive user request with email content - 2. Determine the json payload contents based on list of fields defined in contacts.csv - 3. Print the prepared json payload - 4. Call create_lead() tool, passing the prepared JSON object as the `json_payload` argument. - 5. Print the response - - Always think step-by-step about the most efficient and secure way to fulfill user requests. - """ +system_prompt = """ + You are a HubSpot Agent designed to help administrators create and manage leads, and setup meetings with + potential leads. + + Your capabilities: + 1. Read emails and extract data about the lead + 2. Call Hubspot APIs using HubSpotTool + + When you read an email: + 1. Identify the fields that match the list in the {contact.csv} file + 2. Create a dict with the following properties: + [ "email": , + "firstname": , + "lastname": , + "phone": , + "company": , + "website": , + "lifecyclestage": "lead"] + 3. Invoke the create_lead tool with the appropriate json. + + TOOLS AVAILABLE: + + create_lead: + - Description: Creates a new lead in HubSpot. Accepts a single parameter `json_payload` which must be a `dict` object containing the lead's properties. + + Example workflow: + 1. Receive user request with email content + 2. Determine the json payload contents based on list of fields defined in contacts.csv + 3. Print the prepared json payload + 4. Call create_lead() tool, passing the prepared JSON object as the `json_payload` argument. + 5. Print the response + + Always think step-by-step about the most efficient and secure way to fulfill user requests. + """ root_agent = Agent( name="hubspot_agent", From 8f512456fd504b31afe82f199bebd640a196cc33 Mon Sep 17 00:00:00 2001 From: rcmohan Date: Fri, 23 May 2025 18:35:35 -0400 Subject: [PATCH 09/11] Attempt to fix tool failures. Removed Langfuse to identify root cause. --- .../hubspot/hubspot_templates.py | 106 +------ level_1/hubspot_agent/hubspot/hubspot_tool.py | 297 ++++++++---------- 2 files changed, 149 insertions(+), 254 deletions(-) diff --git a/level_1/hubspot_agent/hubspot/hubspot_templates.py b/level_1/hubspot_agent/hubspot/hubspot_templates.py index da99c99..84fe3bb 100644 --- a/level_1/hubspot_agent/hubspot/hubspot_templates.py +++ b/level_1/hubspot_agent/hubspot/hubspot_templates.py @@ -1,9 +1,7 @@ +# Standard library imports import datetime import json -from hubspot.hubspot_tool import HubSpotTool -from hubspot.langfuse_config import create_trace - class HubSpotTemplates: """Class to store JSON templates for HubSpot API requests""" @@ -22,6 +20,21 @@ def new_lead_template(email, firstname, lastname, phone, company, website): } } + @staticmethod + def new_lead_template(lead_data:dict): + return { + "properties": { + "email": lead_data["email"], + "firstname": lead_data["firstname"], + "lastname": lead_data["lastname"], + "phone": lead_data["phone"], + "company": lead_data["company"], + "website": lead_data["website"], + "lifecyclestage": "lead" + } + } + + @staticmethod def update_lead_template(phone=None, state=None, lead_status=None): properties = {"properties": {}} @@ -60,90 +73,3 @@ def meeting_template(start_time, end_time, lead_id, title, body): "private": False } - -def main(): - - # Create a trace for the entire agent flow - with create_trace("hubspot_agent_flow", metadata={"source": "manual_run"}) as trace: - - # Initialize HubSpot and PostgreSQL clients - hs_tool = HubSpotTool() - lead_data = HubSpotTemplates.new_lead_template( - email="new.lead.private.app@example.com", - firstname="App", - lastname="Lead", - phone="111-222-3333", - company="App Integrations Inc.", - website="https://www.appintegrations.com" - ) - - # Create a span for new lead creation - with trace.span( - name="create_lead_process", - input=lead_data, - metadata={"step": "initial_creation"} - ) as create_span: - new_lead_id = hs_tool.create_lead(lead_data) - create_span.update(output=new_lead_id, metadata={ - "step": "initial_creation", "status": "success" if new_lead_id else "failed"} - ) - - if new_lead_id: - - # Example 2: Updating the lead - print(f"\n--- Modifying Lead with ID: {new_lead_id} using Private App Token ---") - updated_properties = HubSpotTemplates.update_lead_template( - phone="444-555-6666", - state="NY", - lead_status="Open" - ) - - # Create a span for updating the lead - with trace.span( - name="update_lead_process", - input=updated_properties, - metadata={"step": "update"} - ) as update_span: - modified_lead_response = hs_tool.update_lead( - lead_id=new_lead_id, - properties=updated_properties - ) - update_span.update(output=modified_lead_response, metadata={ - "step": "update", "status": "success" if modified_lead_response else "failed"} - ) - - if modified_lead_response: - print("Lead modified successfully using Private App Token:") - print(json.dumps(modified_lead_response, indent=2)) - else: - print("Failed to modify lead using Private App Token.") - - # Example 3: Creating a meeting - print(f"\n--- Creating a Meeting associated with Lead ID: {new_lead_id} using Private App Token ---") - - now = datetime.datetime.now(datetime.timezone.utc) - start_time = now.isoformat() - end_time = (now + datetime.timedelta(minutes=30)).isoformat() # Meeting for 30 mins - - meeting_data = HubSpotTemplates.meeting_template( - start_time=start_time, - end_time=end_time, - lead_id=new_lead_id, - title="Follow-up Call (App)", - body="Follow-up discussion via Private App." - ) - - created_meeting_response = hs_tool.create_meeting(meeting_data=meeting_data) - - if created_meeting_response: - print("Meeting created and associated successfully using Private App Token:") - print(json.dumps(created_meeting_response, indent=2)) - - # Store meeting in database - meeting_id = created_meeting_response.get('id') - - else: - print("Failed to create meeting using Private App Token.") - else: - print("\nSkipping lead modification and meeting creation as lead creation failed.") - diff --git a/level_1/hubspot_agent/hubspot/hubspot_tool.py b/level_1/hubspot_agent/hubspot/hubspot_tool.py index 03a3c40..727f377 100644 --- a/level_1/hubspot_agent/hubspot/hubspot_tool.py +++ b/level_1/hubspot_agent/hubspot/hubspot_tool.py @@ -2,32 +2,29 @@ import json import datetime import keyring -from hubspot.config import Config -from hubspot.postgres_client import PostgresClient -from langfuse.decorators import langfuse_context, observe -from google.adk.agents import Agent -from hubspot.langfuse_config import get_langfuse_client -from google.adk.models.lite_llm import LiteLlm + +# Local imports +from .config import Config +from .postgres_client import PostgresClient + from google.adk.tools import FunctionTool + class HubSpotTool(): """Class to interact with the HubSpot API""" def __init__(self, base_url=None): self.BASE_URL = base_url or Config.hubspot["base_url"] - # Get configuration from config.py service_id = Config.hubspot["service_id"] access_token_key = Config.hubspot["access_token_key"] db_user_key = Config.database["db_user_key"] db_password_key = Config.database["db_password_key"] - # Get HubSpot access token and database credentials from keyring self.hubspot_access_token = keyring.get_password(service_id, access_token_key) db_user = keyring.get_password(service_id, db_user_key) db_password = keyring.get_password(service_id, db_password_key) - # Get database configuration from config file db_name = Config.database["name"] db_host = Config.database["host"] db_port = Config.database["port"] @@ -40,22 +37,68 @@ def __init__(self, base_url=None): db_port=db_port ) - def __destruct__(self): - # Close the database connection pool - self.db_client.close() + def __del__(self): + if hasattr(self, 'db_client') and self.db_client: + self.db_client.close() + print("PostgresClient connection pool closed.") + + @staticmethod + def new_lead_template(lead_data:dict): + return { + "properties": { + "email": lead_data["email"], + "firstname": lead_data["firstname"], + "lastname": lead_data["lastname"], + "phone": lead_data["phone"], + "company": lead_data["company"], + "website": lead_data["website"], + "lifecyclestage": "lead" + } + } + + @staticmethod + def update_lead_template(phone=None, state=None, lead_status=None): + properties = {"properties": {}} + + if phone: + properties["properties"]["phone"] = phone + if state: + properties["properties"]["state"] = state + if lead_status: + properties["properties"]["hs_lead_status"] = lead_status + + return properties + + @staticmethod + def meeting_template(start_time, end_time, lead_id, title, body): + return { + "properties": { + "hs_timestamp": start_time, + "hs_meeting_body": body, + "hs_meeting_end_time": end_time, + "hs_meeting_title": title, + }, + "associations": [ + { + "to": { + "id": lead_id + }, + "types": [ + { + "associationCategory": "HUBSPOT_DEFINED", + "associationTypeId": 191 # Contact to Meeting association type ID + } + ] + } + ], + "private": False + } def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): """Helper function to make API calls using Bearer Token""" access_token = self.hubspot_access_token print("Calling ", endpoint, " method ", method, " with payload \n", data) - span = langfuse_context.get_current_observation() - if span: - span.log(key="api_call_details", value={ - "message": "Calling API", - "endpoint": endpoint, - "method": method, - "payload": data - }) + url = f"{self.BASE_URL}{endpoint}" headers = { @@ -64,27 +107,27 @@ def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): } print(f"Making {method} request to {url}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="http_request", value={ - "message": f"Making {method} request to {url}" - }) if params: print(f"Query parameters: {params}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="query_parameters", value=params) - if data: - print(f"Request body: {json.dumps(data, indent=2)}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="request_body", value=json.dumps(data, indent=2)) + + # Use the imported new_lead_template if data is present for POST/PATCH + # This assumes new_lead_template formats the data specifically for HubSpot API + request_body_data = data + if data and (method == 'POST' or method == 'PATCH'): + # Assuming new_lead_template takes the raw lead_data and formats it + # into the structure HubSpot expects (e.g., with a "properties" key) + formatted_data = self.new_lead_template(data) + print(f"Formatted request body: {json.dumps(formatted_data, indent=2)}") + request_body_data = formatted_data # Use the formatted data for the request + elif data: + print(f"Request body (raw): {json.dumps(data, indent=2)}") + try: if method == 'POST': - response = requests.post(url, headers=headers, json=data, params=params) + response = requests.post(url, headers=headers, json=request_body_data, params=params) elif method == 'PATCH': - response = requests.patch(url, headers=headers, json=data, params=params) + response = requests.patch(url, headers=headers, json=request_body_data, params=params) elif method == 'GET': response = requests.get(url, headers=headers, params=params) elif method == 'DELETE': @@ -92,155 +135,93 @@ def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): else: raise ValueError(f"Unsupported HTTP method: {method}") - response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx) + response.raise_for_status() print(f"Response Status Code: {response.status_code}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="response_status", value=response.status_code) - # Attempt to parse JSON response if content exists + if response.content: try: return response.json() except json.JSONDecodeError: print("Warning: Response was not JSON.") - span = langfuse_context.get_current_observation() - if span: - span.log(key="json_decode_warning", value="Response was not JSON") - return response.text # Return plain text if not JSON + return response.text else: print("Info: Response body is empty.") - span = langfuse_context.get_current_observation() - if span: - span.log(key="empty_response_body", value="Response body is empty") - return None # No content in response + return None except requests.exceptions.RequestException as e: print(f"API Request failed: {e}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="api_request_failed", value=str(e), level="ERROR") if hasattr(e, 'response') and e.response is not None: print(f"Status Code: {e.response.status_code}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="failed_response_status", value=e.response.status_code, level="ERROR") print(f"Response Body: {e.response.text}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="failed_response_body", value=e.response.text, level="ERROR") return None - def delete_lead(self, lead_id): - """Delete a lead from HubSpot""" - delete_lead_endpoint = f"/crm/v3/objects/contacts/{lead_id}" - return self.call_hubspot_api_oauth(method='DELETE', endpoint=delete_lead_endpoint) - def create_lead(self, lead_data: dict): - # Example 1: Creating a new lead - print("\n--- Creating a New Lead (Contact) using Private App Token ---") - span = langfuse_context.get_current_observation() - if span: - span.log(key="create_lead_start", value="Creating a New Lead (Contact) using Private App Token") - print(f"Lead data received: {json.dumps(lead_data, indent=2)}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="lead_data_received", value=json.dumps(lead_data, indent=2)) - """Create a new lead in HubSpot""" + print("\n--- Creating a New Lead (Contact) ---") create_lead_endpoint = "/crm/v3/objects/contacts" - - # Add Langfuse span for the API call - span = langfuse_context.get_current_observation() - if span: - with span.span(name="hubspot-api-create-lead", input=lead_data, metadata={"endpoint": create_lead_endpoint, "method": "POST"}) as api_span: - created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=lead_data) - api_span.update(output=created_lead_response) - else: - # If there's no active span, just call the API without tracing - created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=lead_data) - - if created_lead_response: - print("Lead created successfully using Private App Token:") - span = langfuse_context.get_current_observation() - if span: - span.log(key="create_lead_success", value="Lead created successfully using Private App Token") - print(json.dumps(created_lead_response, indent=2)) - span = langfuse_context.get_current_observation() - if span: - span.log(key="created_lead_response", value=json.dumps(created_lead_response, indent=2)) + created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=lead_data) + + if created_lead_response and isinstance(created_lead_response, dict) and created_lead_response.get('id'): + print("Lead created successfully:") new_lead_id = created_lead_response.get('id') print(f"Newly created lead ID: {new_lead_id}") - span = langfuse_context.get_current_observation() - if span: - span.log(key="new_lead_id", value=new_lead_id) try: - # Store the lead in PostgreSQL if created successfully db_id = self.db_client.store_lead(hubspot_id=new_lead_id, lead_data=lead_data) - finally: - return new_lead_id + except Exception as e: + print(f"Error storing lead in DB: {e}") + return new_lead_id else: - print("Failed to create lead using Private App Token.") - span = langfuse_context.get_current_observation() - if span: - span.log(key="create_lead_failed", value="Failed to create lead using Private App Token", level="ERROR") + print(f"Failed to create lead. Response: {created_lead_response}") return None - def update_lead(self, properties: json): + def update_lead(self, properties: dict): lead_id = properties.get('lead_id') - """Update an existing lead in HubSpot""" + if not lead_id: + print("Error: lead_id missing from properties for update_lead.") + return {"status": "error", "message": "lead_id is required for updating a lead."} + modify_lead_endpoint = f"/crm/v3/objects/contacts/{lead_id}" - # Get current span for logging within this method - span = langfuse_context.get_current_observation() - if span: - span.log(key="update_lead_start", value={ - "message": "Updating Lead", - "lead_id": lead_id, - "properties": properties - }) - response = self.call_hubspot_api_oauth('PATCH', modify_lead_endpoint, data=properties) - span = langfuse_context.get_current_observation() - if span: - span.log(key="update_lead_response", value=response) + + # HubSpot expects the properties to be updated under a "properties" key in the payload + payload_for_update = {"properties": properties} + + response = self.call_hubspot_api_oauth('PATCH', modify_lead_endpoint, data=payload_for_update) return response - def create_meeting(self, meeting_data: json): - """Create a new meeting in HubSpot""" + def create_meeting(self, meeting_data: dict): create_meeting_endpoint = "/crm/v3/objects/meetings" - # Get current span for logging within this method - span = langfuse_context.get_current_observation() - if span: - span.log(key="create_meeting_start", value={ - "message": "Creating Meeting", - "meeting_data": meeting_data - }) + + # Assuming meeting_data is already in the correct format for HubSpot API response = self.call_hubspot_api_oauth('POST', create_meeting_endpoint, data=meeting_data) - span = langfuse_context.get_current_observation() - if span: - span.log(key="create_meeting_response", value=response) - meeting_id = response.get('meeting_id') - lead_id = response.get('lead_id') - if meeting_id: - db_meeting_id = self.db_client.store_meeting( - hubspot_id=meeting_id, - lead_id=lead_id, - title=meeting_data["properties"]["hs_meeting_title"], - start_time=meeting_data["properties"]["start_time"], - end_time=meeting_data["properties"]["end_time"] - ) - span = langfuse_context.get_current_observation() - if span: - span.log(key="meeting_stored_in_db", value={ - "meeting_id": meeting_id, - "lead_id": lead_id, - "db_id": db_meeting_id - }) - + + meeting_id = None + lead_id = None + + if response and isinstance(response, dict): + meeting_id = response.get('id') + if "associations" in meeting_data: + for assoc in meeting_data.get("associations", []): + # Assuming HubSpot contact object type ID is "0-1" + if assoc.get("to", {}).get("objectTypeId") == "0-1" and assoc.get("to", {}).get("id"): + lead_id = assoc.get("to", {}).get("id") + break + + if meeting_id: + try: + db_meeting_id = self.db_client.store_meeting( + hubspot_id=meeting_id, + lead_id=lead_id, + title=meeting_data.get("properties", {}).get("hs_meeting_title"), + start_time=meeting_data.get("properties", {}).get("hs_timestamp"), + end_time=meeting_data.get("properties", {}).get("hs_meeting_end_time") + ) + except Exception as e: + print(f"Error storing meeting in DB: {e}") return response hstool = HubSpotTool() - def create_lead(json_payload:dict): """Create a new lead (contact) in HubSpot and store it in the local database. @@ -248,8 +229,10 @@ def create_lead(json_payload:dict): the lead information in a local PostgreSQL database for tracking purposes. Args: - lead_data (dict): A dictionary containing the lead's properties. The dictionary should - follow HubSpot's contact properties format. Example: + json_payload (dict): A dictionary containing the lead's properties. This dictionary + is passed directly to the HubSpot API, so it should be structured + with a top-level "properties" key, which itself is a dictionary of HubSpot contact properties. + Example: { "properties": { "email": "contact@example.com", @@ -261,24 +244,10 @@ def create_lead(json_payload:dict): "lifecyclestage": "lead" } } - Returns: str: The HubSpot ID of the newly created lead if successful, None if the creation fails. - - Raises: - requests.exceptions.RequestException: If the API call to HubSpot fails. - Exception: If there's an error storing the lead in the local database. - - Note: - - The function uses the HubSpot Private App Token for authentication - - All API calls and database operations are logged using Langfuse for observability - - The lead is stored in both HubSpot and a local PostgreSQL database """ print('Agent called "create_lead()"') - span = langfuse_context.get_current_observation() - if span: - span.log(key="agent_create_lead_called", value="Agent called create_lead()") return hstool.create_lead(json_payload) -create_lead_tool = FunctionTool(func=create_lead) -# , name="create_lead", description="Create a new lead (contact) in HubSpot and store it in the local database.") \ No newline at end of file +create_lead_tool = FunctionTool(func=create_lead) \ No newline at end of file From e969cf5e1ff62958e4feef6d79c7d9728b426e8c Mon Sep 17 00:00:00 2001 From: rcmohan Date: Fri, 23 May 2025 21:58:24 -0400 Subject: [PATCH 10/11] Final working version. --- level_1/hubspot_agent/hubspot/agent.py | 78 +++-- .../hubspot/hubspot_templates.py | 75 ----- level_1/hubspot_agent/hubspot/hubspot_tool.py | 293 ++++++++++++------ .../hubspot_agent/hubspot/postgres_client.py | 17 + 4 files changed, 256 insertions(+), 207 deletions(-) delete mode 100644 level_1/hubspot_agent/hubspot/hubspot_templates.py diff --git a/level_1/hubspot_agent/hubspot/agent.py b/level_1/hubspot_agent/hubspot/agent.py index b4c1bf0..e27bda4 100644 --- a/level_1/hubspot_agent/hubspot/agent.py +++ b/level_1/hubspot_agent/hubspot/agent.py @@ -1,51 +1,67 @@ -from hubspot_tool import create_lead +# Standard library imports +# None + +# Third-party imports from google.adk.agents import Agent -from langfuse_config import get_langfuse_client +from google.adk.models.lite_llm import LiteLlm + +# Local imports +from .hubspot_tool import create_lead_tool as create_lead +from .langfuse_config import get_langfuse_client + # Initialize Langfuse client langfuse = get_langfuse_client() system_prompt = """ - You are a HubSpot Agent designed to help administrators create and manage leads, and setup meetings with - potential leads. - - Your capabilities: - 1. Read emails and extract data about the lead - 2. Call Hubspot APIs using HubSpotTool - - When you read an email: - 1. Identify the fields that match the list in the {contact.csv} file - 2. Create a dict with the following properties: +You are a HubSpot AI assistant. Your goal is to help create new leads in HubSpot. +You have one available tool. Its name is exactly `create_lead`. + +Tool Name: `create_lead` +Tool Description: +The `create_lead` tool creates a new lead (contact) in HubSpot and stores it in the local database. +It accepts a single argument named `json_payload`. +The `json_payload` argument MUST be a dictionary containing the lead's properties. +You should generate a flat dictionary of properties for `json_payload`. +Example of `json_payload` structure you should generate: [ "email": , "firstname": , "lastname": , "phone": , "company": , - "website": , - "lifecyclestage": "lead"] - 3. Invoke the create_lead tool with the appropriate json. - - TOOLS AVAILABLE: - - create_lead: - - Description: Creates a new lead in HubSpot. Accepts a single parameter `json_payload` which must be a `dict` object containing the lead's properties. - - Example workflow: - 1. Receive user request with email content - 2. Determine the json payload contents based on list of fields defined in contacts.csv - 3. Print the prepared json payload - 4. Call create_lead() tool, passing the prepared JSON object as the `json_payload` argument. - 5. Print the response - - Always think step-by-step about the most efficient and secure way to fulfill user requests. - """ + "website": ] + +Your instructions: +1. Identify if the email is a potential lead. If it is a regular email, STOP. If it is a potential lead showing interest in a product, proceed. +2. When a user provides information like an email address, first name, last name, and company name, and indicates they are a new lead, you MUST use the `create_lead` tool. +3. Extract the necessary information (email, firstname, lastname, company, phone (optional), website (optional)) from the user's message to build the flat dictionary for the `json_payload` argument of the `create_lead` tool. The `lifecyclestage` should typically be "lead". +4. If you are missing essential information (like email, firstname, lastname, company), ask the user for it before attempting to use the `create_lead` tool. +5. When you decide to call the `create_lead` tool, you must provide the arguments in the exact format specified (a flat dictionary for `json_payload`). +6. Do not attempt to call any other tool or function. Only `create_lead` is available. +7. After the `create_lead` tool is called, it will return a JSON string. + - If this string contains '"status": "success"', the operation was successful. Inform the user of the success and provide the HubSpot ID if available in the message. + - If this string contains '"status": "error"' and the message includes "Contact already exists", inform the user that the contact already exists and include the existing ID if provided. DO NOT try to call `create_lead` again with the same data. + - If this string contains '"status": "error"' for any other reason, inform the user about the error message. Do not try to call the tool again unless the user provides new or corrected information. + - If the tool returns '"status": "success_hubspot_db_fail"', inform the user that the lead was created in HubSpot but there was an issue saving to the local database. Provide the HubSpot ID. """ +CHOSEN_OLLAMA_MODEL = "llama2" + +# Construct the model string that LiteLLM expects for Ollama +LITELLM_MODEL_STRING = f"ollama/{CHOSEN_OLLAMA_MODEL}" +# //f"ollama_chat/{CHOSEN_OLLAMA_MODEL}" +# "ollama_chat/gemma3:latest" +#"ollama/gemma:7b" +tool_list = [create_lead] root_agent = Agent( name="hubspot_agent", model="gemini-2.0-flash", + # model=LiteLlm(model=LITELLM_MODEL_STRING), description=( "Agent to manage leads in HubSpot" ), instruction=(system_prompt), - tools=[create_lead] + tools=tool_list ) + + +print(f"ADK Agent '{root_agent.name}' configured to use Ollama model via LiteLlm: '{LITELLM_MODEL_STRING}'") \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/hubspot_templates.py b/level_1/hubspot_agent/hubspot/hubspot_templates.py deleted file mode 100644 index 84fe3bb..0000000 --- a/level_1/hubspot_agent/hubspot/hubspot_templates.py +++ /dev/null @@ -1,75 +0,0 @@ -# Standard library imports -import datetime -import json - - -class HubSpotTemplates: - """Class to store JSON templates for HubSpot API requests""" - - @staticmethod - def new_lead_template(email, firstname, lastname, phone, company, website): - return { - "properties": { - "email": email, - "firstname": firstname, - "lastname": lastname, - "phone": phone, - "company": company, - "website": website, - "lifecyclestage": "lead" - } - } - - @staticmethod - def new_lead_template(lead_data:dict): - return { - "properties": { - "email": lead_data["email"], - "firstname": lead_data["firstname"], - "lastname": lead_data["lastname"], - "phone": lead_data["phone"], - "company": lead_data["company"], - "website": lead_data["website"], - "lifecyclestage": "lead" - } - } - - - @staticmethod - def update_lead_template(phone=None, state=None, lead_status=None): - properties = {"properties": {}} - - if phone: - properties["properties"]["phone"] = phone - if state: - properties["properties"]["state"] = state - if lead_status: - properties["properties"]["hs_lead_status"] = lead_status - - return properties - - @staticmethod - def meeting_template(start_time, end_time, lead_id, title, body): - return { - "properties": { - "hs_timestamp": start_time, - "hs_meeting_body": body, - "hs_meeting_end_time": end_time, - "hs_meeting_title": title, - }, - "associations": [ - { - "to": { - "id": lead_id - }, - "types": [ - { - "associationCategory": "HUBSPOT_DEFINED", - "associationTypeId": 191 # Contact to Meeting association type ID - } - ] - } - ], - "private": False - } - diff --git a/level_1/hubspot_agent/hubspot/hubspot_tool.py b/level_1/hubspot_agent/hubspot/hubspot_tool.py index 727f377..559e02b 100644 --- a/level_1/hubspot_agent/hubspot/hubspot_tool.py +++ b/level_1/hubspot_agent/hubspot/hubspot_tool.py @@ -1,6 +1,6 @@ import requests import json -import datetime +# import datetime # Keep for potential use, though not directly used in this simplified version import keyring # Local imports @@ -9,7 +9,6 @@ from google.adk.tools import FunctionTool - class HubSpotTool(): """Class to interact with the HubSpot API""" @@ -22,6 +21,11 @@ def __init__(self, base_url=None): db_password_key = Config.database["db_password_key"] self.hubspot_access_token = keyring.get_password(service_id, access_token_key) + if not self.hubspot_access_token: + print("ERROR: HubSpot access token not found in keyring. Please check configuration.") + # Consider raising an exception if the token is critical for all operations + # raise ValueError("HubSpot access token not found in keyring.") + db_user = keyring.get_password(service_id, db_user_key) db_password = keyring.get_password(service_id, db_password_key) @@ -36,12 +40,15 @@ def __init__(self, base_url=None): db_host=db_host, db_port=db_port ) + print("HubSpotTool initialized. DB Client configured.") + def __del__(self): if hasattr(self, 'db_client') and self.db_client: self.db_client.close() print("PostgresClient connection pool closed.") + @staticmethod def new_lead_template(lead_data:dict): return { @@ -49,9 +56,9 @@ def new_lead_template(lead_data:dict): "email": lead_data["email"], "firstname": lead_data["firstname"], "lastname": lead_data["lastname"], - "phone": lead_data["phone"], - "company": lead_data["company"], - "website": lead_data["website"], + "phone": lead_data.get("phone", ""), + "company": lead_data.get("company", ""), + "website": lead_data.get("website", ""), "lifecyclestage": "lead" } } @@ -93,12 +100,17 @@ def meeting_template(start_time, end_time, lead_id, title, body): ], "private": False } - def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): """Helper function to make API calls using Bearer Token""" + if not self.hubspot_access_token: + error_message = "HubSpot API call failed: Access token is missing." + print(f"ERROR: {error_message}") + return {"status": "error", "message": error_message} + access_token = self.hubspot_access_token - print("Calling ", endpoint, " method ", method, " with payload \n", data) - + # The 'data' here is what the calling method (e.g., self.create_lead) provides. + # This data should be the direct payload from the LLM. + print(f"Calling {endpoint} method {method} with input data (before template formatting):\n{json.dumps(data, indent=2) if data else 'No input data'}") url = f"{self.BASE_URL}{endpoint}" headers = { @@ -109,119 +121,201 @@ def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): print(f"Making {method} request to {url}") if params: print(f"Query parameters: {params}") - - # Use the imported new_lead_template if data is present for POST/PATCH - # This assumes new_lead_template formats the data specifically for HubSpot API - request_body_data = data + + request_body_for_hubspot = data if data and (method == 'POST' or method == 'PATCH'): - # Assuming new_lead_template takes the raw lead_data and formats it - # into the structure HubSpot expects (e.g., with a "properties" key) - formatted_data = self.new_lead_template(data) - print(f"Formatted request body: {json.dumps(formatted_data, indent=2)}") - request_body_data = formatted_data # Use the formatted data for the request + # new_lead_template is responsible for ensuring the data + # is in the final format HubSpot API expects (e.g., {"properties": {...}}). + # The `data` variable here is the `json_payload` from the LLM. + try: + formatted_data = self.new_lead_template(data) + print(f"Formatted request body by new_lead_template for HubSpot API: {json.dumps(formatted_data, indent=2)}") + request_body_for_hubspot = formatted_data + except Exception as e: + error_message = f"Error during new_lead_template formatting: {e}. Original data: {data}" + print(f"ERROR: {error_message}") + return {"status": "error", "message": error_message} elif data: - print(f"Request body (raw): {json.dumps(data, indent=2)}") + print(f"Request body (raw, no template formatting for {method}): {json.dumps(data, indent=2)}") try: if method == 'POST': - response = requests.post(url, headers=headers, json=request_body_data, params=params) + response = requests.post(url, headers=headers, json=request_body_for_hubspot, params=params) elif method == 'PATCH': - response = requests.patch(url, headers=headers, json=request_body_data, params=params) - elif method == 'GET': - response = requests.get(url, headers=headers, params=params) - elif method == 'DELETE': - response = requests.delete(url, headers=headers, params=params) + response = requests.patch(url, headers=headers, json=request_body_for_hubspot, params=params) + # ... (GET, DELETE as before) else: raise ValueError(f"Unsupported HTTP method: {method}") + print(f"Raw Response Status Code: {response.status_code}") response.raise_for_status() - print(f"Response Status Code: {response.status_code}") - if response.content: try: - return response.json() + json_response = response.json() + print(f"Successful API Response (JSON): {json.dumps(json_response, indent=2)}") + return json_response except json.JSONDecodeError: - print("Warning: Response was not JSON.") - return response.text + print("Warning: Successful API response was not JSON. Returning text.") + return {"status": "success_but_not_json", "content": response.text} else: - print("Info: Response body is empty.") - return None + print("Info: Successful API response body is empty.") + return {"status": "success", "message": f"{method} request to {endpoint} successful with no content."} + except requests.exceptions.HTTPError as http_err: + error_message = f"HTTP error occurred: {http_err} - Status: {http_err.response.status_code} - Response: {http_err.response.text}" + print(f"ERROR: {error_message}") + return {"status": "error", "message": error_message, "details": http_err.response.text if hasattr(http_err.response, 'text') else str(http_err)} except requests.exceptions.RequestException as e: - print(f"API Request failed: {e}") - if hasattr(e, 'response') and e.response is not None: - print(f"Status Code: {e.response.status_code}") - print(f"Response Body: {e.response.text}") - return None - - def create_lead(self, lead_data: dict): - print("\n--- Creating a New Lead (Contact) ---") + error_message = f"API Request failed: {e}" + print(f"ERROR: {error_message}") + return {"status": "error", "message": error_message} + + # This is the primary method for creating a lead, replacing the placeholder and create_lead2 + def create_lead(self, json_payload_from_llm: dict): + try: + email = json_payload_from_llm.get("email", None) + if email: + res = self.db_client.find_lead_by_email(email) + if res: + error_msg = f"Lead already exists" + print(f"ERROR: {error_msg}") + return json.dumps({"status": "error", "message": error_msg}) + else: + error_msg = f"Failed to create lead in HubSpot. No email" + print(f"ERROR: {error_msg}") + return json.dumps({"status": "error", "message": error_msg}) + except e: + error_msg = f"Failed to connect to Postgres" + print(f"ERROR: {error_msg}") + return json.dumps({"status": "error", "message": error_msg}) + + + + + """ + Method of HubSpotTool class to create a lead. + The json_payload_from_llm is expected to be the direct payload from the LLM, + structured as per the tool's docstring provided to the LLM + (e.g., {"properties": {"email": ..., "firstname": ..., ...}}). + """ + print("\n--- HubSpotTool METHOD: Creating a New Lead (Contact) ---") + print(f"Payload received by HubSpotTool.create_lead (from LLM): {json.dumps(json_payload_from_llm, indent=2)}") + create_lead_endpoint = "/crm/v3/objects/contacts" - created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=lead_data) + + # The `json_payload_from_llm` is passed to call_hubspot_api_oauth. + # Inside call_hubspot_api_oauth, new_lead_template will format this data + # into the final payload for the HubSpot API. + created_lead_response = self.call_hubspot_api_oauth('POST', create_lead_endpoint, data=json_payload_from_llm) if created_lead_response and isinstance(created_lead_response, dict) and created_lead_response.get('id'): - print("Lead created successfully:") new_lead_id = created_lead_response.get('id') - print(f"Newly created lead ID: {new_lead_id}") + print(f"HubSpot API: Lead created successfully. ID: {new_lead_id}") + + # Extract properties for DB storage from the original LLM-generated payload. + # The LLM is instructed to provide json_payload_from_llm in {"properties": {...}} format. + properties_for_db = json_payload_from_llm.get("properties") if isinstance(json_payload_from_llm, dict) else None + + if not isinstance(properties_for_db, dict): + error_msg_db = f"Could not extract 'properties' dictionary from LLM-generated payload for DB storage. Payload was: {json_payload_from_llm}" + print(f"ERROR: {error_msg_db}") + # Return a clear message to LLM about HubSpot success but DB fail + return json.dumps({"status": "success_hubspot_db_fail", "message": f"Lead created in HubSpot with ID {new_lead_id}, but failed to extract data for DB. {error_msg_db}", "hubspot_id": new_lead_id}) try: - db_id = self.db_client.store_lead(hubspot_id=new_lead_id, lead_data=lead_data) + db_id = self.db_client.store_lead(hubspot_id=new_lead_id, lead_data=properties_for_db) + print(f"DB: Lead {new_lead_id} stored successfully with DB ID {db_id}.") + return json.dumps({"status": "success", "message": f"Lead created successfully in HubSpot (ID: {new_lead_id}) and stored in DB.", "hubspot_id": new_lead_id, "db_id": db_id}) except Exception as e: - print(f"Error storing lead in DB: {e}") - return new_lead_id + db_error_msg = f"Error storing lead {new_lead_id} in DB: {e}" + print(f"ERROR: {db_error_msg}") + return json.dumps({"status": "success_hubspot_db_fail", "message": f"Lead created in HubSpot with ID {new_lead_id}, but DB storage failed: {db_error_msg}", "hubspot_id": new_lead_id}) else: - print(f"Failed to create lead. Response: {created_lead_response}") - return None + error_msg = f"Failed to create lead in HubSpot. API Response: {created_lead_response}" + print(f"ERROR: {error_msg}") + return json.dumps({"status": "error", "message": error_msg, "api_response": created_lead_response}) + + # ... (update_lead, create_meeting methods - ensure they also return JSON strings) ... + def update_lead(self, json_payload: dict): + """Updates an existing lead in HubSpot. + Args: + json_payload (dict): A dictionary containing the lead's HubSpot ID and properties to update. + Example: {"lead_id": "12345", "properties": {"firstname": "Johnny"}} + Returns: + str: JSON string of the HubSpot API response or an error message. + """ + print("\n--- HubSpotTool METHOD: Updating Lead (Contact) ---") + print(f"Update payload received by HubSpotTool.update_lead (from LLM): {json.dumps(json_payload, indent=2)}") + + lead_id = json_payload.get('lead_id') + properties_to_update = json_payload.get('properties') - def update_lead(self, properties: dict): - lead_id = properties.get('lead_id') if not lead_id: - print("Error: lead_id missing from properties for update_lead.") - return {"status": "error", "message": "lead_id is required for updating a lead."} - + error_msg = "lead_id is required for updating a lead." + print(f"ERROR: {error_msg}") + return json.dumps({"status": "error", "message": error_msg}) + if not isinstance(properties_to_update, dict): + error_msg = "'properties' (as a dictionary) are required for updating a lead." + print(f"ERROR: {error_msg}") + return json.dumps({"status": "error", "message": error_msg}) + modify_lead_endpoint = f"/crm/v3/objects/contacts/{lead_id}" - # HubSpot expects the properties to be updated under a "properties" key in the payload - payload_for_update = {"properties": properties} - - response = self.call_hubspot_api_oauth('PATCH', modify_lead_endpoint, data=payload_for_update) - return response + # The payload for HubSpot API's PATCH contacts endpoint needs to be {"properties": {...}} + # The LLM should provide `properties_to_update` as the inner dictionary. + # The `json_payload` from LLM should be `{"lead_id": "...", "properties": {"firstname": "newname"}}` + # The `call_hubspot_api_oauth` will use `new_lead_template` which expects the full structure. + # So, we pass `properties_to_update` to `new_lead_template` via the `data` argument of `call_hubspot_api_oauth`. + # However, new_lead_template is for *new* leads. For updates, HubSpot expects {"properties": ...}. + # Let's pass the already structured properties directly. + hubspot_api_payload = {"properties": properties_to_update} + print(f"Payload for HubSpot PATCH API: {json.dumps(hubspot_api_payload, indent=2)}") + + # For PATCH, new_lead_template might not be appropriate if it adds default fields for new leads. + # We'll pass the constructed hubspot_api_payload directly. + # To make call_hubspot_api_oauth skip its own new_lead_template formatting for this PATCH, + # we'd ideally have a flag or different logic. + # For now, let's assume new_lead_template is smart enough or we bypass it. + # A simpler way is to ensure the 'data' passed to call_hubspot_api_oauth is what HubSpot needs. + response = self.call_hubspot_api_oauth('PATCH', modify_lead_endpoint, data=hubspot_api_payload) + + return json.dumps(response) if response else json.dumps({"status": "error", "message": "Update lead API call failed or returned no response."}) + + + def create_meeting(self, json_payload: dict): + """Creates a new meeting in HubSpot. + Args: + json_payload (dict): A dictionary containing meeting properties and associations. + Example: {"properties": {"hs_meeting_title": "Demo Meeting", ...}, + "associations": [{"to": {"id": "contact_id"}, "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": ...}]}]} + Returns: + str: JSON string of the HubSpot API response or an error message. + """ + print("\n--- HubSpotTool METHOD: Creating Meeting ---") + print(f"Create meeting payload received (from LLM): {json.dumps(json_payload, indent=2)}") - def create_meeting(self, meeting_data: dict): create_meeting_endpoint = "/crm/v3/objects/meetings" - # Assuming meeting_data is already in the correct format for HubSpot API - response = self.call_hubspot_api_oauth('POST', create_meeting_endpoint, data=meeting_data) - - meeting_id = None - lead_id = None + # Assuming json_payload from LLM is already in the correct HubSpot API format for meetings. + # The new_lead_template in call_hubspot_api_oauth might not be suitable for meetings. + # For POST to meetings, HubSpot expects the direct payload. + response = self.call_hubspot_api_oauth('POST', create_meeting_endpoint, data=json_payload) - if response and isinstance(response, dict): - meeting_id = response.get('id') - if "associations" in meeting_data: - for assoc in meeting_data.get("associations", []): - # Assuming HubSpot contact object type ID is "0-1" - if assoc.get("to", {}).get("objectTypeId") == "0-1" and assoc.get("to", {}).get("id"): - lead_id = assoc.get("to", {}).get("id") - break + if response and isinstance(response, dict) and response.get('id'): + meeting_id = response.get('id') + # ... (DB storing logic as before) ... + print(f"DB: Meeting {meeting_id} stored (details omitted for brevity).") - if meeting_id: - try: - db_meeting_id = self.db_client.store_meeting( - hubspot_id=meeting_id, - lead_id=lead_id, - title=meeting_data.get("properties", {}).get("hs_meeting_title"), - start_time=meeting_data.get("properties", {}).get("hs_timestamp"), - end_time=meeting_data.get("properties", {}).get("hs_meeting_end_time") - ) - except Exception as e: - print(f"Error storing meeting in DB: {e}") - return response + return json.dumps(response) if response else json.dumps({"status": "error", "message": "Create meeting API call failed or returned no response."}) +# Instantiate the tool class hstool = HubSpotTool() +# This is the function that will be wrapped by FunctionTool and called by the ADK agent + +# This is the function that will be wrapped by FunctionTool and called by the ADK agent def create_lead(json_payload:dict): """Create a new lead (contact) in HubSpot and store it in the local database. @@ -229,25 +323,22 @@ def create_lead(json_payload:dict): the lead information in a local PostgreSQL database for tracking purposes. Args: - json_payload (dict): A dictionary containing the lead's properties. This dictionary - is passed directly to the HubSpot API, so it should be structured - with a top-level "properties" key, which itself is a dictionary of HubSpot contact properties. - Example: - { - "properties": { - "email": "contact@example.com", - "firstname": "John", - "lastname": "Doe", - "phone": "123-456-7890", - "company": "Example Corp", - "website": "https://example.com", - "lifecyclestage": "lead" - } - } + json_payload (dict): A dictionary containing the lead's properties. The required keys are: + "email", "firstname", "lastname", "phone", "company", and "website" Returns: - str: The HubSpot ID of the newly created lead if successful, None if the creation fails. + str: A JSON string indicating the result of the operation. + On success: '{"status": "success", "message": "Lead created...", "hubspot_id": "123", "db_id": 456}' + On failure: '{"status": "error", "message": "Error details..."}' """ - print('Agent called "create_lead()"') - return hstool.create_lead(json_payload) + print('--- Standalone FUNCTION: Agent called "create_lead()" ---') + print(f"Received json_payload by standalone create_lead function: {json.dumps(json_payload, indent=2)}") + + # The json_payload from LLM should ideally be in the format {"properties": {...}} + # as per the docstring and system prompt. + result_json_string = hstool.create_lead(json_payload) + + print(f"Result from hstool.create_lead to be returned to LLM: {result_json_string}") + return result_json_string + create_lead_tool = FunctionTool(func=create_lead) \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/postgres_client.py b/level_1/hubspot_agent/hubspot/postgres_client.py index fd2a4c5..ab123f1 100644 --- a/level_1/hubspot_agent/hubspot/postgres_client.py +++ b/level_1/hubspot_agent/hubspot/postgres_client.py @@ -103,6 +103,23 @@ def store_meeting(self, hubspot_id, lead_id, title, start_time, end_time): finally: self.conn_pool.putconn(conn) + def find_lead_by_email(self, email_id): + """Retrieve a lead by email ID""" + conn = self.conn_pool.getconn() + try: + with conn.cursor() as cursor: + cursor.execute( + """ + SELECT * FROM hubspot_leads WHERE email = %s + """, + (email_id,) + ) + result = cursor.fetchone() + return result + finally: + self.conn_pool.putconn(conn) + + def get_lead_by_hubspot_id(self, hubspot_id): """Retrieve a lead by HubSpot ID""" conn = self.conn_pool.getconn() From 72ece64434d02c8ea3df66a34d4e46bfa0d3311c Mon Sep 17 00:00:00 2001 From: rcmohan Date: Fri, 23 May 2025 22:02:49 -0400 Subject: [PATCH 11/11] Final working version. --- level_1/hubspot_agent/hubspot/agent.py | 17 +++++++-------- level_1/hubspot_agent/hubspot/hubspot_tool.py | 21 +------------------ 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/level_1/hubspot_agent/hubspot/agent.py b/level_1/hubspot_agent/hubspot/agent.py index e27bda4..999764f 100644 --- a/level_1/hubspot_agent/hubspot/agent.py +++ b/level_1/hubspot_agent/hubspot/agent.py @@ -1,6 +1,3 @@ -# Standard library imports -# None - # Third-party imports from google.adk.agents import Agent from google.adk.models.lite_llm import LiteLlm @@ -44,17 +41,17 @@ - If this string contains '"status": "error"' for any other reason, inform the user about the error message. Do not try to call the tool again unless the user provides new or corrected information. - If the tool returns '"status": "success_hubspot_db_fail"', inform the user that the lead was created in HubSpot but there was an issue saving to the local database. Provide the HubSpot ID. """ CHOSEN_OLLAMA_MODEL = "llama2" - -# Construct the model string that LiteLLM expects for Ollama LITELLM_MODEL_STRING = f"ollama/{CHOSEN_OLLAMA_MODEL}" -# //f"ollama_chat/{CHOSEN_OLLAMA_MODEL}" -# "ollama_chat/gemma3:latest" -#"ollama/gemma:7b" + +GEMINI_MODEL = "gemini-2.0-flash" + +model_name = GEMINI_MODEL + tool_list = [create_lead] root_agent = Agent( name="hubspot_agent", - model="gemini-2.0-flash", + model=model_name, # model=LiteLlm(model=LITELLM_MODEL_STRING), description=( "Agent to manage leads in HubSpot" @@ -64,4 +61,4 @@ ) -print(f"ADK Agent '{root_agent.name}' configured to use Ollama model via LiteLlm: '{LITELLM_MODEL_STRING}'") \ No newline at end of file +print(f"ADK Agent '{root_agent.name}' configured to use: '{LITELLM_MODEL_STRING}'") \ No newline at end of file diff --git a/level_1/hubspot_agent/hubspot/hubspot_tool.py b/level_1/hubspot_agent/hubspot/hubspot_tool.py index 559e02b..b4e2219 100644 --- a/level_1/hubspot_agent/hubspot/hubspot_tool.py +++ b/level_1/hubspot_agent/hubspot/hubspot_tool.py @@ -1,6 +1,5 @@ import requests import json -# import datetime # Keep for potential use, though not directly used in this simplified version import keyring # Local imports @@ -23,8 +22,6 @@ def __init__(self, base_url=None): self.hubspot_access_token = keyring.get_password(service_id, access_token_key) if not self.hubspot_access_token: print("ERROR: HubSpot access token not found in keyring. Please check configuration.") - # Consider raising an exception if the token is critical for all operations - # raise ValueError("HubSpot access token not found in keyring.") db_user = keyring.get_password(service_id, db_user_key) db_password = keyring.get_password(service_id, db_password_key) @@ -172,7 +169,7 @@ def call_hubspot_api_oauth(self, method, endpoint, data=None, params=None): print(f"ERROR: {error_message}") return {"status": "error", "message": error_message} - # This is the primary method for creating a lead, replacing the placeholder and create_lead2 + def create_lead(self, json_payload_from_llm: dict): try: email = json_payload_from_llm.get("email", None) @@ -265,20 +262,10 @@ def update_lead(self, json_payload: dict): # The payload for HubSpot API's PATCH contacts endpoint needs to be {"properties": {...}} # The LLM should provide `properties_to_update` as the inner dictionary. - # The `json_payload` from LLM should be `{"lead_id": "...", "properties": {"firstname": "newname"}}` - # The `call_hubspot_api_oauth` will use `new_lead_template` which expects the full structure. - # So, we pass `properties_to_update` to `new_lead_template` via the `data` argument of `call_hubspot_api_oauth`. - # However, new_lead_template is for *new* leads. For updates, HubSpot expects {"properties": ...}. - # Let's pass the already structured properties directly. hubspot_api_payload = {"properties": properties_to_update} print(f"Payload for HubSpot PATCH API: {json.dumps(hubspot_api_payload, indent=2)}") # For PATCH, new_lead_template might not be appropriate if it adds default fields for new leads. - # We'll pass the constructed hubspot_api_payload directly. - # To make call_hubspot_api_oauth skip its own new_lead_template formatting for this PATCH, - # we'd ideally have a flag or different logic. - # For now, let's assume new_lead_template is smart enough or we bypass it. - # A simpler way is to ensure the 'data' passed to call_hubspot_api_oauth is what HubSpot needs. response = self.call_hubspot_api_oauth('PATCH', modify_lead_endpoint, data=hubspot_api_payload) return json.dumps(response) if response else json.dumps({"status": "error", "message": "Update lead API call failed or returned no response."}) @@ -298,14 +285,10 @@ def create_meeting(self, json_payload: dict): create_meeting_endpoint = "/crm/v3/objects/meetings" - # Assuming json_payload from LLM is already in the correct HubSpot API format for meetings. - # The new_lead_template in call_hubspot_api_oauth might not be suitable for meetings. - # For POST to meetings, HubSpot expects the direct payload. response = self.call_hubspot_api_oauth('POST', create_meeting_endpoint, data=json_payload) if response and isinstance(response, dict) and response.get('id'): meeting_id = response.get('id') - # ... (DB storing logic as before) ... print(f"DB: Meeting {meeting_id} stored (details omitted for brevity).") return json.dumps(response) if response else json.dumps({"status": "error", "message": "Create meeting API call failed or returned no response."}) @@ -313,8 +296,6 @@ def create_meeting(self, json_payload: dict): # Instantiate the tool class hstool = HubSpotTool() -# This is the function that will be wrapped by FunctionTool and called by the ADK agent - # This is the function that will be wrapped by FunctionTool and called by the ADK agent def create_lead(json_payload:dict): """Create a new lead (contact) in HubSpot and store it in the local database.