From c666f35cd03cdda8c27b4ebe77fa99ec55f66ea9 Mon Sep 17 00:00:00 2001 From: nlp8899 <142526738+nlp8899@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:13:23 -0400 Subject: [PATCH] create a hubspot connector (#482) --- backend/.gitignore | 1 + backend/danswer/configs/constants.py | 1 + backend/danswer/connectors/factory.py | 2 + .../danswer/connectors/hubspot/__init__.py | 0 .../danswer/connectors/hubspot/connector.py | 138 +++++++++++ backend/requirements/default.txt | 1 + web/public/HubSpot.png | Bin 0 -> 14062 bytes web/src/app/admin/connectors/hubspot/page.tsx | 223 ++++++++++++++++++ web/src/app/admin/layout.tsx | 10 + web/src/components/icons/icons.tsx | 16 ++ web/src/components/search/Filters.tsx | 1 + web/src/components/source.tsx | 7 + web/src/lib/types.ts | 7 + 13 files changed, 407 insertions(+) create mode 100644 backend/danswer/connectors/hubspot/__init__.py create mode 100644 backend/danswer/connectors/hubspot/connector.py create mode 100644 web/public/HubSpot.png create mode 100644 web/src/app/admin/connectors/hubspot/page.tsx diff --git a/backend/.gitignore b/backend/.gitignore index b75d7f4d75..38ac9f33e7 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -8,3 +8,4 @@ qdrant-data/ typesense-data/ .env vespa-app.zip +dynamic_config_storage/ diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index 058b62fbe0..6bd72f18b3 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -59,6 +59,7 @@ class DocumentSource(str, Enum): NOTION = "notion" ZULIP = "zulip" LINEAR = "linear" + HUBSPOT = "hubspot" class DocumentIndexType(str, Enum): diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index 6de50524a0..a3df62c576 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -22,6 +22,7 @@ from danswer.connectors.slack.connector import SlackLoadConnector from danswer.connectors.slack.connector import SlackPollConnector from danswer.connectors.web.connector import WebConnector from danswer.connectors.zulip.connector import ZulipConnector +from danswer.connectors.hubspot.connector import HubSpotConnector class ConnectorMissingException(Exception): @@ -50,6 +51,7 @@ def identify_connector_class( DocumentSource.ZULIP: ZulipConnector, DocumentSource.GURU: GuruConnector, DocumentSource.LINEAR: LinearConnector, + DocumentSource.HUBSPOT: HubSpotConnector, } connector_by_source = connector_map.get(source, {}) diff --git a/backend/danswer/connectors/hubspot/__init__.py b/backend/danswer/connectors/hubspot/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/danswer/connectors/hubspot/connector.py b/backend/danswer/connectors/hubspot/connector.py new file mode 100644 index 0000000000..0c8b84ae0e --- /dev/null +++ b/backend/danswer/connectors/hubspot/connector.py @@ -0,0 +1,138 @@ +import requests +import json +from typing import Any +from datetime import datetime +from hubspot import HubSpot +from danswer.configs.app_configs import INDEX_BATCH_SIZE +from danswer.configs.constants import DocumentSource +from danswer.connectors.interfaces import GenerateDocumentsOutput +from danswer.connectors.interfaces import LoadConnector +from danswer.connectors.interfaces import PollConnector +from danswer.connectors.interfaces import SecondsSinceUnixEpoch +from danswer.connectors.models import ConnectorMissingCredentialError +from danswer.connectors.models import Document +from danswer.connectors.models import Section +from danswer.utils.logger import setup_logger + +HUBSPOT_BASE_URL = "https://app.hubspot.com/contacts/" +HUBSPOT_API_URL = "https://api.hubapi.com/integrations/v1/me" + +logger = setup_logger() + +class HubSpotConnector(LoadConnector, PollConnector): + def __init__(self, batch_size: int = INDEX_BATCH_SIZE, access_token: str | None = None) -> None: + self.batch_size = batch_size + self.access_token = access_token + self.portal_id = None + self.ticket_base_url = HUBSPOT_BASE_URL + + def get_portal_id(self) -> str: + headers = { + 'Authorization': f'Bearer {self.access_token}', + 'Content-Type': 'application/json' + } + + response = requests.get(HUBSPOT_API_URL, headers=headers) + if response.status_code != 200: + raise Exception("Error fetching portal ID") + + data = response.json() + return data["portalId"] + + def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: + self.access_token = credentials["hubspot_access_token"] + + if self.access_token: + self.portal_id = self.get_portal_id() + self.ticket_base_url = f"{HUBSPOT_BASE_URL}{self.portal_id}/ticket/" + + return None + + def _process_tickets( + self, start: datetime | None = None, end: datetime | None = None + ) -> GenerateDocumentsOutput: + if self.access_token is None: + raise ConnectorMissingCredentialError("HubSpot") + + api_client = HubSpot(access_token=self.access_token) + all_tickets = api_client.crm.tickets.get_all(associations=['contacts', 'notes']) + + doc_batch: list[Document] = [] + + for ticket in all_tickets: + updated_at = ticket.updated_at.replace(tzinfo=None) + if start is not None and updated_at < start: + continue + if end is not None and updated_at > end: + continue + + title = ticket.properties["subject"] + link = self.ticket_base_url + ticket.id + content_text = title + "\n" + ticket.properties["content"] + + associated_emails = [] + associated_notes = [] + + if ticket.associations: + contacts = ticket.associations.get("contacts") + notes = ticket.associations.get("notes") + + if contacts: + for contact in contacts.results: + contact = api_client.crm.contacts.basic_api.get_by_id(contact_id=contact.id) + associated_emails.append(contact.properties["email"]) + + if notes: + for note in notes.results: + note = api_client.crm.objects.notes.basic_api.get_by_id(note_id=note.id, properties=["content", "hs_body_preview"]) + associated_notes.append(note.properties["hs_body_preview"]) + + associated_emails = " ,".join(associated_emails) + associated_notes = " ".join(associated_notes) + + content_text = f"{content_text}\n emails: {associated_emails} \n notes: {associated_notes}" + + doc_batch.append( + Document( + id=ticket.id, + sections=[Section(link=link, text=content_text)], + source=DocumentSource.HUBSPOT, + semantic_identifier=title, + metadata={}, + ) + ) + + if len(doc_batch) >= self.batch_size: + yield doc_batch + doc_batch = [] + + if doc_batch: + yield doc_batch + + def load_from_state(self) -> GenerateDocumentsOutput: + return self._process_tickets() + + def poll_source( + self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch + ) -> GenerateDocumentsOutput: + start_datetime = datetime.fromtimestamp(start) + end_datetime = datetime.fromtimestamp(end) + return self._process_tickets(start_datetime, end_datetime) + + + +if __name__ == "__main__": + import os + import time + test_connector = HubSpotConnector() + test_connector.load_credentials({ + "hubspot_access_token": os.environ["HUBSPOT_ACCESS_TOKEN"] + }) + all_docs = test_connector.load_from_state() + + current = time.time() + one_day_ago = current - 24 * 60 * 60 # 1 day + latest_docs = test_connector.poll_source(one_day_ago, current) + print(latest_docs) + + diff --git a/backend/requirements/default.txt b/backend/requirements/default.txt index 0cc8effa90..64620cf19e 100644 --- a/backend/requirements/default.txt +++ b/backend/requirements/default.txt @@ -53,3 +53,4 @@ transformers==4.30.1 typesense==0.15.1 uvicorn==0.21.1 zulip==0.8.2 +hubspot-api-client==8.1.0 diff --git a/web/public/HubSpot.png b/web/public/HubSpot.png new file mode 100644 index 0000000000000000000000000000000000000000..e4112aecd0dfcf52688c44348c984fecf3aa2eb6 GIT binary patch literal 14062 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 H*uMOKXV4>H literal 0 HcmV?d00001 diff --git a/web/src/app/admin/connectors/hubspot/page.tsx b/web/src/app/admin/connectors/hubspot/page.tsx new file mode 100644 index 0000000000..04559e4320 --- /dev/null +++ b/web/src/app/admin/connectors/hubspot/page.tsx @@ -0,0 +1,223 @@ +"use client"; + +import * as Yup from "yup"; +import { HubSpotIcon, TrashIcon } from "@/components/icons/icons"; +import { TextFormField } from "@/components/admin/connectors/Field"; +import { HealthCheckBanner } from "@/components/health/healthcheck"; +import { CredentialForm } from "@/components/admin/connectors/CredentialForm"; +import { + Credential, + ConnectorIndexingStatus, + HubSpotConfig, + HubSpotCredentialJson, +} from "@/lib/types"; +import useSWR, { useSWRConfig } from "swr"; +import { fetcher } from "@/lib/fetcher"; +import { LoadingAnimation } from "@/components/Loading"; +import { adminDeleteCredential, linkCredential } from "@/lib/credential"; +import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; +import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable"; +import { usePopup } from "@/components/admin/connectors/Popup"; +import { usePublicCredentials } from "@/lib/hooks"; + +const Main = () => { + const { popup, setPopup } = usePopup(); + + const { mutate } = useSWRConfig(); + const { + data: connectorIndexingStatuses, + isLoading: isConnectorIndexingStatusesLoading, + error: isConnectorIndexingStatusesError, + } = useSWR[]>( + "/api/manage/admin/connector/indexing-status", + fetcher + ); + + const { + data: credentialsData, + isLoading: isCredentialsLoading, + isValidating: isCredentialsValidating, + error: isCredentialsError, + refreshCredentials, + } = usePublicCredentials(); + + if ( + isConnectorIndexingStatusesLoading || + isCredentialsLoading || + isCredentialsValidating + ) { + return ; + } + + if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { + return
Failed to load connectors
; + } + + if (isCredentialsError || !credentialsData) { + return
Failed to load credentials
; + } + + const hubSpotConnectorIndexingStatuses: ConnectorIndexingStatus< + HubSpotConfig, + HubSpotCredentialJson + >[] = connectorIndexingStatuses.filter( + (connectorIndexingStatus) => + connectorIndexingStatus.connector.source === "hubspot" + ); + const hubSpotCredential: Credential = credentialsData.filter( + (credential) => credential.credential_json?.hubspot_access_token + )[0]; + + return ( + <> + {popup} +

+ This connector allows you to sync all your HubSpot Tickets into Danswer. +

+ +

+ Step 1: Provide your Credentials +

+ + {hubSpotCredential ? ( + <> +
+

Existing Access Token:

+

+ {hubSpotCredential.credential_json?.hubspot_access_token} +

+ +
+ + ) : ( + <> +

+ To use the HubSpot connector, provide the HubSpot Access Token. +

+
+ + formBody={ + <> + + + } + validationSchema={Yup.object().shape({ + hubspot_access_token: Yup.string().required( + "Please enter your HubSpot Access Token" + ) + })} + initialValues={{ + hubspot_access_token: "", + }} + onSubmit={(isSuccess) => { + if (isSuccess) { + refreshCredentials(); + } + }} + /> +
+ + )} + +

+ Step 2: Start indexing! +

+ {hubSpotCredential ? ( + !hubSpotConnectorIndexingStatuses.length ? ( + <> +

+ Click the button below to start indexing! We will pull the latest + tickets from HubSpot every 10 minutes. +

+
+ + nameBuilder={() => "HubSpotConnector"} + source="hubspot" + inputType="poll" + formBody={null} + validationSchema={Yup.object().shape({})} + initialValues={{}} + refreshFreq={10 * 60} // 10 minutes + onSubmit={async (isSuccess, responseJson) => { + if (isSuccess && responseJson) { + await linkCredential(responseJson.id, hubSpotCredential.id); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + /> +
+ + ) : ( + <> +

+ HubSpot connector is setup! We are pulling the latest tickets from HubSpot + every 10 minutes. +

+ + connectorIndexingStatuses={hubSpotConnectorIndexingStatuses} + liveCredential={hubSpotCredential} + getCredential={(credential) => { + return ( +
+

{credential.credential_json.hubspot_access_token}

+
+ ); + }} + onCredentialLink={async (connectorId) => { + if (hubSpotCredential) { + await linkCredential(connectorId, hubSpotCredential.id); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + onUpdate={() => + mutate("/api/manage/admin/connector/indexing-status") + } + /> + + ) + ) : ( + <> +

+ Please provide your access token in Step 1 first! Once done with + that, you can then start indexing all your HubSpot tickets. +

+ + )} + + ); +}; + +export default function Page() { + return ( +
+
+ +
+
+ +

HubSpot

+
+
+
+ ); +} diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index c911b3b83f..58bffb98d0 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -19,6 +19,7 @@ import { LinearIcon, UsersIcon, ThumbsUpIcon, + HubSpotIcon, BookmarkIcon, CPUIcon, } from "@/components/icons/icons"; @@ -192,6 +193,15 @@ export default async function AdminLayout({ ), link: "/admin/connectors/file", }, + { + name: ( +
+ +
HubSpot
+
+ ), + link: "/admin/connectors/hubspot", + }, ], }, { diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 4b5d2344fc..34321051bd 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -41,6 +41,7 @@ import confluenceSVG from "../../../public/Confluence.svg"; import guruIcon from "../../../public/Guru.svg"; import zulipIcon from "../../../public/Zulip.png"; import linearIcon from "../../../public/Linear.png"; +import hubSpotIcon from "../../../public/HubSpot.png"; interface IconProps { size?: number; @@ -421,3 +422,18 @@ export const GuruIcon = ({ Logo ); + +export const HubSpotIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( +
+ Logo +
+ ); +}; diff --git a/web/src/components/search/Filters.tsx b/web/src/components/search/Filters.tsx index 14e4e9ad6b..c8be96c354 100644 --- a/web/src/components/search/Filters.tsx +++ b/web/src/components/search/Filters.tsx @@ -27,6 +27,7 @@ const sources: Source[] = [ { displayName: "Notion", internalName: "notion" }, { displayName: "Zulip", internalName: "zulip" }, { displayName: "Linear", internalName: "linear" }, + { displayName: "HubSpot", internalName: "hubspot" }, ]; interface SourceSelectorProps { diff --git a/web/src/components/source.tsx b/web/src/components/source.tsx index 683c6c77fa..6db1120bd6 100644 --- a/web/src/components/source.tsx +++ b/web/src/components/source.tsx @@ -14,6 +14,7 @@ import { SlabIcon, SlackIcon, ZulipIcon, + HubSpotIcon, } from "./icons/icons"; interface SourceMetadata { @@ -108,6 +109,12 @@ export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => { displayName: "Linear", adminPageLink: "/admin/connectors/linear", }; + case "hubspot": + return { + icon: HubSpotIcon, + displayName: "HubSpot", + adminPageLink: "/admin/connectors/hubspot", + }; default: throw new Error("Invalid source type"); } diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 594f819d8c..cfaf565d13 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -21,6 +21,7 @@ export type ValidSources = | "guru" | "zulip" | "linear" + | "hubspot" | "file"; export type ValidInputTypes = "load_state" | "poll" | "event"; export type ValidStatuses = @@ -106,6 +107,8 @@ export interface ZulipConfig { export interface NotionConfig {} +export interface HubSpotConfig {} + export interface IndexAttemptSnapshot { status: ValidStatuses | null; num_docs_indexed: number; @@ -204,6 +207,10 @@ export interface LinearCredentialJson { linear_api_key: string; } +export interface HubSpotCredentialJson { + hubspot_access_token: string; +} + // DELETION export interface DeletionAttemptSnapshot {