mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-26 17:51:54 +01:00
Gong Connector (#529)
--------- Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
parent
c658ffd0b6
commit
7d3f8b7c8c
@ -60,6 +60,7 @@ class DocumentSource(str, Enum):
|
||||
ZULIP = "zulip"
|
||||
LINEAR = "linear"
|
||||
HUBSPOT = "hubspot"
|
||||
GONG = "gong"
|
||||
|
||||
|
||||
class DocumentIndexType(str, Enum):
|
||||
|
@ -61,9 +61,9 @@ if __name__ == "__main__":
|
||||
### Additional Required Changes:
|
||||
#### Backend Changes
|
||||
- Add a new type to
|
||||
[DocumentSource](https://github.com/danswer-ai/danswer/blob/main/backend/danswer/configs/constants.py#L20)
|
||||
[DocumentSource](https://github.com/danswer-ai/danswer/blob/main/backend/danswer/configs/constants.py)
|
||||
- Add a mapping from DocumentSource (and optionally connector type) to the right connector class
|
||||
[here](https://github.com/danswer-ai/danswer/blob/main/backend/danswer/connectors/factory.py#L32)
|
||||
[here](https://github.com/danswer-ai/danswer/blob/main/backend/danswer/connectors/factory.py#L33)
|
||||
|
||||
#### Frontend Changes
|
||||
- Create the new connector directory and admin page under `danswer/web/src/app/admin/connectors/`
|
||||
|
@ -7,6 +7,7 @@ from danswer.connectors.confluence.connector import ConfluenceConnector
|
||||
from danswer.connectors.danswer_jira.connector import JiraConnector
|
||||
from danswer.connectors.file.connector import LocalFileConnector
|
||||
from danswer.connectors.github.connector import GithubConnector
|
||||
from danswer.connectors.gong.connector import GongConnector
|
||||
from danswer.connectors.google_drive.connector import GoogleDriveConnector
|
||||
from danswer.connectors.guru.connector import GuruConnector
|
||||
from danswer.connectors.hubspot.connector import HubSpotConnector
|
||||
@ -52,6 +53,7 @@ def identify_connector_class(
|
||||
DocumentSource.GURU: GuruConnector,
|
||||
DocumentSource.LINEAR: LinearConnector,
|
||||
DocumentSource.HUBSPOT: HubSpotConnector,
|
||||
DocumentSource.GONG: GongConnector,
|
||||
}
|
||||
connector_by_source = connector_map.get(source, {})
|
||||
|
||||
|
@ -13,6 +13,7 @@ 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
|
||||
@ -68,7 +69,7 @@ def _convert_issue_to_document(issue: Issue) -> Document:
|
||||
)
|
||||
|
||||
|
||||
class GithubConnector(LoadConnector):
|
||||
class GithubConnector(LoadConnector, PollConnector):
|
||||
def __init__(
|
||||
self,
|
||||
repo_owner: str,
|
||||
|
0
backend/danswer/connectors/gong/__init__.py
Normal file
0
backend/danswer/connectors/gong/__init__.py
Normal file
275
backend/danswer/connectors/gong/connector.py
Normal file
275
backend/danswer/connectors/gong/connector.py
Normal file
@ -0,0 +1,275 @@
|
||||
import base64
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
|
||||
import requests
|
||||
|
||||
from danswer.configs.app_configs import CONTINUE_ON_CONNECTOR_FAILURE
|
||||
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
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
GONG_BASE_URL = "https://us-34014.api.gong.io"
|
||||
|
||||
|
||||
class GongConnector(LoadConnector, PollConnector):
|
||||
def __init__(
|
||||
self,
|
||||
workspaces: list[str] | None = None,
|
||||
batch_size: int = INDEX_BATCH_SIZE,
|
||||
use_end_time: bool = False,
|
||||
continue_on_fail: bool = CONTINUE_ON_CONNECTOR_FAILURE,
|
||||
hide_user_info: bool = False,
|
||||
) -> None:
|
||||
self.workspaces = workspaces
|
||||
self.batch_size: int = batch_size
|
||||
self.continue_on_fail = continue_on_fail
|
||||
self.auth_token_basic: str | None = None
|
||||
self.use_end_time = use_end_time
|
||||
self.hide_user_info = hide_user_info
|
||||
|
||||
def _get_auth_header(self) -> dict[str, str]:
|
||||
if self.auth_token_basic is None:
|
||||
raise ConnectorMissingCredentialError("Gong")
|
||||
|
||||
return {"Authorization": f"Basic {self.auth_token_basic}"}
|
||||
|
||||
def _get_workspace_id_map(self) -> dict[str, str]:
|
||||
url = f"{GONG_BASE_URL}/v2/workspaces"
|
||||
response = requests.get(url, headers=self._get_auth_header())
|
||||
response.raise_for_status()
|
||||
|
||||
workspaces_details = response.json().get("workspaces")
|
||||
return {workspace["name"]: workspace["id"] for workspace in workspaces_details}
|
||||
|
||||
def _get_transcript_batches(
|
||||
self, start_datetime: str | None = None, end_datetime: str | None = None
|
||||
) -> Generator[list[dict[str, Any]], None, None]:
|
||||
url = f"{GONG_BASE_URL}/v2/calls/transcript"
|
||||
body: dict[str, dict] = {"filter": {}}
|
||||
if start_datetime:
|
||||
body["filter"]["fromDateTime"] = start_datetime
|
||||
if end_datetime:
|
||||
body["filter"]["toDateTime"] = end_datetime
|
||||
|
||||
# The batch_ids in the previous method appears to be batches of call_ids to process
|
||||
# In this method, we will retrieve transcripts for them in batches.
|
||||
transcripts: list[dict[str, Any]] = []
|
||||
workspace_list = self.workspaces or [None] # type: ignore
|
||||
workspace_map = self._get_workspace_id_map() if self.workspaces else {}
|
||||
|
||||
for workspace in workspace_list:
|
||||
if workspace:
|
||||
logger.info(f"Updating workspace: {workspace}")
|
||||
workspace_id = workspace_map.get(workspace)
|
||||
if not workspace_id:
|
||||
logger.error(f"Invalid workspace: {workspace}")
|
||||
if not self.continue_on_fail:
|
||||
raise ValueError(f"Invalid workspace: {workspace}")
|
||||
continue
|
||||
body["filter"]["workspaceId"] = workspace_id
|
||||
else:
|
||||
if "workspaceId" in body["filter"]:
|
||||
del body["filter"]["workspaceId"]
|
||||
|
||||
while True:
|
||||
response = requests.post(
|
||||
url, headers=self._get_auth_header(), json=body
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
call_transcripts = data.get("callTranscripts", [])
|
||||
transcripts.extend(call_transcripts)
|
||||
|
||||
while len(transcripts) >= self.batch_size:
|
||||
yield transcripts[: self.batch_size]
|
||||
transcripts = transcripts[self.batch_size :]
|
||||
|
||||
cursor = data.get("records", {}).get("cursor")
|
||||
if cursor:
|
||||
body["cursor"] = cursor
|
||||
else:
|
||||
break
|
||||
|
||||
if transcripts:
|
||||
yield transcripts
|
||||
|
||||
def _get_call_details_by_ids(self, call_ids: list[str]) -> dict:
|
||||
url = f"{GONG_BASE_URL}/v2/calls/extensive"
|
||||
|
||||
body = {
|
||||
"filter": {"callIds": call_ids},
|
||||
"contentSelector": {"exposedFields": {"parties": True}},
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=self._get_auth_header(), json=body)
|
||||
response.raise_for_status()
|
||||
|
||||
calls = response.json().get("calls")
|
||||
call_to_metadata = {}
|
||||
for call in calls:
|
||||
call_to_metadata[call["metaData"]["id"]] = call
|
||||
|
||||
return call_to_metadata
|
||||
|
||||
@staticmethod
|
||||
def _parse_parties(parties: list[dict]) -> dict[str, str]:
|
||||
id_mapping = {}
|
||||
for party in parties:
|
||||
name = party.get("name")
|
||||
email = party.get("emailAddress")
|
||||
|
||||
if name and email:
|
||||
full_identifier = f"{name} ({email})"
|
||||
elif name:
|
||||
full_identifier = name
|
||||
elif email:
|
||||
full_identifier = email
|
||||
else:
|
||||
full_identifier = "Unknown"
|
||||
|
||||
id_mapping[party["speakerId"]] = full_identifier
|
||||
|
||||
return id_mapping
|
||||
|
||||
def _fetch_calls(
|
||||
self, start_datetime: str | None = None, end_datetime: str | None = None
|
||||
) -> GenerateDocumentsOutput:
|
||||
for transcript_batch in self._get_transcript_batches(
|
||||
start_datetime, end_datetime
|
||||
):
|
||||
doc_batch: list[Document] = []
|
||||
|
||||
call_ids = cast(
|
||||
list[str],
|
||||
[t.get("callId") for t in transcript_batch if t.get("callId")],
|
||||
)
|
||||
call_details_map = self._get_call_details_by_ids(call_ids)
|
||||
|
||||
for transcript in transcript_batch:
|
||||
call_id = transcript.get("callId")
|
||||
|
||||
if not call_id or call_id not in call_details_map:
|
||||
logger.error(
|
||||
f"Couldn't get call information for Call ID: {call_id}"
|
||||
)
|
||||
if not self.continue_on_fail:
|
||||
raise RuntimeError(
|
||||
f"Couldn't get call information for Call ID: {call_id}"
|
||||
)
|
||||
continue
|
||||
|
||||
call_details = call_details_map[call_id]
|
||||
|
||||
call_metadata = call_details["metaData"]
|
||||
call_parties = call_details["parties"]
|
||||
|
||||
id_to_name_map = self._parse_parties(call_parties)
|
||||
|
||||
# Keeping a separate dict here in case the parties info is incomplete
|
||||
speaker_to_name: dict[str, str] = {}
|
||||
|
||||
transcript_text = ""
|
||||
call_title = call_metadata["title"]
|
||||
if call_title:
|
||||
transcript_text += f"Call Title: {call_title}\n\n"
|
||||
|
||||
call_purpose = call_metadata["purpose"]
|
||||
if call_purpose:
|
||||
transcript_text += f"Call Description: {call_purpose}\n\n"
|
||||
|
||||
contents = transcript["transcript"]
|
||||
for segment in contents:
|
||||
speaker_id = segment.get("speakerId", "")
|
||||
if speaker_id not in speaker_to_name:
|
||||
if self.hide_user_info:
|
||||
speaker_to_name[
|
||||
speaker_id
|
||||
] = f"User {len(speaker_to_name) + 1}"
|
||||
else:
|
||||
speaker_to_name[speaker_id] = id_to_name_map.get(
|
||||
speaker_id, "Unknown"
|
||||
)
|
||||
|
||||
speaker_name = speaker_to_name[speaker_id]
|
||||
|
||||
sentences = segment.get("sentences", {})
|
||||
monolog = " ".join(
|
||||
[sentence.get("text", "") for sentence in sentences]
|
||||
)
|
||||
transcript_text += f"{speaker_name}: {monolog}\n\n"
|
||||
|
||||
doc_batch.append(
|
||||
Document(
|
||||
id=call_id,
|
||||
sections=[
|
||||
Section(link=call_metadata["url"], text=transcript_text)
|
||||
],
|
||||
source=DocumentSource.GONG,
|
||||
# Should not ever be Untitled as a call cannot be made without a Title
|
||||
semantic_identifier=call_title or "Untitled",
|
||||
metadata={"Start Time": call_metadata["started"]},
|
||||
)
|
||||
)
|
||||
yield doc_batch
|
||||
|
||||
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
|
||||
combined = (
|
||||
f'{credentials["gong_access_key"]}:{credentials["gong_access_key_secret"]}'
|
||||
)
|
||||
self.auth_token_basic = base64.b64encode(combined.encode("utf-8")).decode(
|
||||
"utf-8"
|
||||
)
|
||||
return None
|
||||
|
||||
def load_from_state(self) -> GenerateDocumentsOutput:
|
||||
return self._fetch_calls()
|
||||
|
||||
def poll_source(
|
||||
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
|
||||
) -> GenerateDocumentsOutput:
|
||||
# Because these are meeting start times, the meeting needs to end and be processed
|
||||
# so adding a 1 day buffer and fetching by default till current time
|
||||
start_datetime = datetime.fromtimestamp(start, tz=timezone.utc)
|
||||
start_one_day_offset = start_datetime - timedelta(days=1)
|
||||
start_time = start_one_day_offset.isoformat()
|
||||
end_time = (
|
||||
datetime.fromtimestamp(end, tz=timezone.utc).isoformat()
|
||||
if self.use_end_time
|
||||
else None
|
||||
)
|
||||
|
||||
return self._fetch_calls(start_time, end_time)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import time
|
||||
|
||||
connector = GongConnector()
|
||||
connector.load_credentials(
|
||||
{
|
||||
"gong_access_key": os.environ["GONG_ACCESS_KEY"],
|
||||
"gong_access_key_secret": os.environ["GONG_ACCESS_KEY_SECRET"],
|
||||
}
|
||||
)
|
||||
|
||||
current = time.time()
|
||||
one_day_ago = current - 24 * 60 * 60 # 1 day
|
||||
latest_docs = connector.poll_source(one_day_ago, current)
|
||||
print(next(latest_docs))
|
BIN
web/public/Gong.png
Normal file
BIN
web/public/Gong.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
@ -213,14 +213,18 @@ const Main = () => {
|
||||
{
|
||||
header: "Url",
|
||||
key: "url",
|
||||
getValue: (connector) => (
|
||||
getValue: (ccPairStatus) => (
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href={
|
||||
connector.connector_specific_config.wiki_page_url
|
||||
ccPairStatus.connector.connector_specific_config
|
||||
.wiki_page_url
|
||||
}
|
||||
>
|
||||
{connector.connector_specific_config.wiki_page_url}
|
||||
{
|
||||
ccPairStatus.connector.connector_specific_config
|
||||
.wiki_page_url
|
||||
}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
|
@ -216,8 +216,8 @@ const Main = () => {
|
||||
{
|
||||
header: "File Names",
|
||||
key: "file_names",
|
||||
getValue: (connector) =>
|
||||
connector.connector_specific_config.file_locations
|
||||
getValue: (ccPairStatus) =>
|
||||
ccPairStatus.connector.connector_specific_config.file_locations
|
||||
.map(getNameFromPath)
|
||||
.join(", "),
|
||||
},
|
||||
|
@ -156,8 +156,11 @@ const Main = () => {
|
||||
{
|
||||
header: "Repository",
|
||||
key: "repository",
|
||||
getValue: (connector) =>
|
||||
`${connector.connector_specific_config.repo_owner}/${connector.connector_specific_config.repo_name}`,
|
||||
getValue: (ccPairStatus) => {
|
||||
const connectorConfig =
|
||||
ccPairStatus.connector.connector_specific_config;
|
||||
return `${connectorConfig.repo_owner}/${connectorConfig.repo_name}`;
|
||||
},
|
||||
},
|
||||
]}
|
||||
onUpdate={() =>
|
||||
|
257
web/src/app/admin/connectors/gong/page.tsx
Normal file
257
web/src/app/admin/connectors/gong/page.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
"use client";
|
||||
|
||||
import * as Yup from "yup";
|
||||
import { GongIcon, TrashIcon } from "@/components/icons/icons";
|
||||
import {
|
||||
TextFormField,
|
||||
TextArrayFieldBuilder,
|
||||
} from "@/components/admin/connectors/Field";
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||
import {
|
||||
Credential,
|
||||
ConnectorIndexingStatus,
|
||||
GongConfig,
|
||||
GongCredentialJson,
|
||||
} 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<ConnectorIndexingStatus<any, any>[]>(
|
||||
"/api/manage/admin/connector/indexing-status",
|
||||
fetcher
|
||||
);
|
||||
|
||||
const {
|
||||
data: credentialsData,
|
||||
isLoading: isCredentialsLoading,
|
||||
isValidating: isCredentialsValidating,
|
||||
error: isCredentialsError,
|
||||
refreshCredentials,
|
||||
} = usePublicCredentials();
|
||||
|
||||
if (
|
||||
isConnectorIndexingStatusesLoading ||
|
||||
isCredentialsLoading ||
|
||||
isCredentialsValidating
|
||||
) {
|
||||
return <LoadingAnimation text="Loading" />;
|
||||
}
|
||||
|
||||
if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) {
|
||||
return <div>Failed to load connectors</div>;
|
||||
}
|
||||
|
||||
if (isCredentialsError || !credentialsData) {
|
||||
return <div>Failed to load credentials</div>;
|
||||
}
|
||||
|
||||
const gongConnectorIndexingStatuses: ConnectorIndexingStatus<
|
||||
GongConfig,
|
||||
GongCredentialJson
|
||||
>[] = connectorIndexingStatuses.filter(
|
||||
(connectorIndexingStatus) =>
|
||||
connectorIndexingStatus.connector.source === "gong"
|
||||
);
|
||||
const gongCredential: Credential<GongCredentialJson> | undefined =
|
||||
credentialsData.find(
|
||||
(credential) => credential.credential_json?.gong_access_key
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<p className="text-sm">
|
||||
This connector allows you to sync all your Gong Transcripts into
|
||||
Danswer. More details on how to setup the Gong connector can be found in{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href="https://docs.danswer.dev/connectors/gong"
|
||||
target="_blank"
|
||||
>
|
||||
this guide.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your API Access info
|
||||
</h2>
|
||||
|
||||
{gongCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing Access Key Secret: </p>
|
||||
<p className="ml-1 italic my-auto max-w-md truncate">
|
||||
{gongCredential.credential_json?.gong_access_key_secret}
|
||||
</p>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
onClick={async () => {
|
||||
if (gongConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Must delete all connectors before deleting credentials",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await adminDeleteCredential(gongCredential.id);
|
||||
refreshCredentials();
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
<CredentialForm<GongCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
<TextFormField name="gong_access_key" label="Access Key:" />
|
||||
<TextFormField
|
||||
name="gong_access_key_secret"
|
||||
label="Access Key Secret:"
|
||||
type="password"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
validationSchema={Yup.object().shape({
|
||||
gong_access_key: Yup.string().required(
|
||||
"Please enter your Gong Access Key"
|
||||
),
|
||||
gong_access_key_secret: Yup.string().required(
|
||||
"Please enter your Gong Access Key Secret"
|
||||
),
|
||||
})}
|
||||
initialValues={{
|
||||
gong_access_key: "",
|
||||
gong_access_key_secret: "",
|
||||
}}
|
||||
onSubmit={(isSuccess) => {
|
||||
if (isSuccess) {
|
||||
refreshCredentials();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Which Workspaces do you want to make searchable?
|
||||
</h2>
|
||||
|
||||
{gongConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
We pull the latest transcript every <b>10</b> minutes.
|
||||
</p>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<GongConfig, GongCredentialJson>
|
||||
connectorIndexingStatuses={gongConnectorIndexingStatuses}
|
||||
liveCredential={gongCredential}
|
||||
getCredential={(credential) =>
|
||||
credential.credential_json.gong_access_key
|
||||
}
|
||||
specialColumns={[
|
||||
{
|
||||
header: "Workspaces",
|
||||
key: "workspaces",
|
||||
getValue: (ccPairStatus) =>
|
||||
ccPairStatus.connector.connector_specific_config
|
||||
.workspaces &&
|
||||
ccPairStatus.connector.connector_specific_config.workspaces
|
||||
.length > 0
|
||||
? ccPairStatus.connector.connector_specific_config.workspaces.join(
|
||||
", "
|
||||
)
|
||||
: "",
|
||||
},
|
||||
]}
|
||||
includeName={true}
|
||||
onUpdate={() =>
|
||||
mutate("/api/manage/admin/connector/indexing-status")
|
||||
}
|
||||
onCredentialLink={async (connectorId) => {
|
||||
if (gongCredential) {
|
||||
await linkCredential(connectorId, gongCredential.id);
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gongCredential ? (
|
||||
<>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<h2 className="font-bold mb-3">Create a new Gong Connector</h2>
|
||||
<ConnectorForm<GongConfig>
|
||||
nameBuilder={(values) =>
|
||||
values.workspaces
|
||||
? `GongConnector-${values.workspaces.join("_")}`
|
||||
: `GongConnector-All`
|
||||
}
|
||||
source="gong"
|
||||
inputType="poll"
|
||||
formBodyBuilder={TextArrayFieldBuilder({
|
||||
name: "workspaces",
|
||||
label: "Workspaces:",
|
||||
subtext:
|
||||
"Specify 0 or more workspaces to index. Be sure to use the EXACT workspace name from Gong. " +
|
||||
"If no workspaces are specified, transcripts from all workspaces will be indexed.",
|
||||
})}
|
||||
validationSchema={Yup.object().shape({
|
||||
workspaces: Yup.array().of(
|
||||
Yup.string().required("Workspace names must be strings")
|
||||
),
|
||||
})}
|
||||
initialValues={{
|
||||
workspaces: [],
|
||||
}}
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={gongCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm">
|
||||
Please provide your API Access Info in Step 1 first! Once done with
|
||||
that, you can then start indexing all your Gong transcripts.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<GongIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Gong</h1>
|
||||
</div>
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -220,16 +220,18 @@ const Main = () => {
|
||||
{
|
||||
header: "Url",
|
||||
key: "url",
|
||||
getValue: (connector) => (
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href={
|
||||
connector.connector_specific_config.jira_project_url
|
||||
}
|
||||
>
|
||||
{connector.connector_specific_config.jira_project_url}
|
||||
</a>
|
||||
),
|
||||
getValue: (ccPairStatus) => {
|
||||
const connectorConfig =
|
||||
ccPairStatus.connector.connector_specific_config;
|
||||
return (
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href={connectorConfig.jira_project_url}
|
||||
>
|
||||
{connectorConfig.jira_project_url}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
onUpdate={() =>
|
||||
|
@ -185,14 +185,18 @@ const Main = () => {
|
||||
{
|
||||
header: "Url",
|
||||
key: "url",
|
||||
getValue: (connector) => (
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href={connector.connector_specific_config.base_url}
|
||||
>
|
||||
{connector.connector_specific_config.base_url}
|
||||
</a>
|
||||
),
|
||||
getValue: (ccPairStatus) => {
|
||||
const connectorConfig =
|
||||
ccPairStatus.connector.connector_specific_config;
|
||||
return (
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href={connectorConfig.base_url}
|
||||
>
|
||||
{connectorConfig.base_url}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
onUpdate={() =>
|
||||
|
@ -154,17 +154,20 @@ const MainSection = () => {
|
||||
{
|
||||
header: "Workspace",
|
||||
key: "workspace",
|
||||
getValue: (connector) =>
|
||||
connector.connector_specific_config.workspace,
|
||||
getValue: (ccPairStatus) =>
|
||||
ccPairStatus.connector.connector_specific_config.workspace,
|
||||
},
|
||||
{
|
||||
header: "Channels",
|
||||
key: "channels",
|
||||
getValue: (connector) =>
|
||||
connector.connector_specific_config.channels &&
|
||||
connector.connector_specific_config.channels.length > 0
|
||||
? connector.connector_specific_config.channels.join(", ")
|
||||
: "",
|
||||
getValue: (ccPairStatus) => {
|
||||
const connectorConfig =
|
||||
ccPairStatus.connector.connector_specific_config;
|
||||
return connectorConfig.channels &&
|
||||
connectorConfig.channels.length > 0
|
||||
? connectorConfig.channels.join(", ")
|
||||
: "";
|
||||
},
|
||||
},
|
||||
]}
|
||||
onUpdate={() =>
|
||||
|
@ -119,24 +119,28 @@ export default function Web() {
|
||||
{
|
||||
header: "Base URL",
|
||||
key: "base_url",
|
||||
getValue: (connector) => (
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href={connector.connector_specific_config.base_url}
|
||||
>
|
||||
{connector.connector_specific_config.base_url}
|
||||
</a>
|
||||
),
|
||||
getValue: (ccPairConfig) => {
|
||||
const connectorConfig =
|
||||
ccPairConfig.connector.connector_specific_config;
|
||||
return (
|
||||
<a className="text-blue-500" href={connectorConfig.base_url}>
|
||||
{connectorConfig.base_url}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Scrape Method",
|
||||
key: "web_connector_type",
|
||||
getValue: (connector) =>
|
||||
connector.connector_specific_config.web_connector_type
|
||||
getValue: (ccPairStatus) => {
|
||||
const connectorConfig =
|
||||
ccPairStatus.connector.connector_specific_config;
|
||||
return connectorConfig.web_connector_type
|
||||
? SCRAPE_TYPE_TO_PRETTY_NAME[
|
||||
connector.connector_specific_config.web_connector_type
|
||||
connectorConfig.web_connector_type
|
||||
]
|
||||
: "Recursive",
|
||||
: "Recursive";
|
||||
},
|
||||
},
|
||||
]}
|
||||
onUpdate={() => mutate("/api/manage/admin/connector/indexing-status")}
|
||||
|
@ -151,14 +151,14 @@ const MainSection = () => {
|
||||
{
|
||||
header: "Realm name",
|
||||
key: "realm_name",
|
||||
getValue: (connector) =>
|
||||
connector.connector_specific_config.realm_name,
|
||||
getValue: (ccPairStatus) =>
|
||||
ccPairStatus.connector.connector_specific_config.realm_name,
|
||||
},
|
||||
{
|
||||
header: "Realm url",
|
||||
key: "realm_url",
|
||||
getValue: (connector) =>
|
||||
connector.connector_specific_config.realm_url,
|
||||
getValue: (ccPairStatus) =>
|
||||
ccPairStatus.connector.connector_specific_config.realm_url,
|
||||
},
|
||||
]}
|
||||
onUpdate={() =>
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
BookstackIcon,
|
||||
ConfluenceIcon,
|
||||
GuruIcon,
|
||||
GongIcon,
|
||||
FileIcon,
|
||||
JiraIcon,
|
||||
SlabIcon,
|
||||
@ -159,6 +160,15 @@ export default async function AdminLayout({
|
||||
),
|
||||
link: "/admin/connectors/guru",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GongIcon size={16} />
|
||||
<div className="ml-1">Gong</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/gong",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
|
@ -97,15 +97,24 @@ export function StatusRow<ConnectorConfigType, ConnectorCredentialType>({
|
||||
);
|
||||
}
|
||||
|
||||
interface ColumnSpecification<ConnectorConfigType> {
|
||||
export interface ColumnSpecification<
|
||||
ConnectorConfigType,
|
||||
ConnectorCredentialType
|
||||
> {
|
||||
header: string;
|
||||
key: string;
|
||||
getValue: (
|
||||
connector: Connector<ConnectorConfigType>
|
||||
ccPairStatus: ConnectorIndexingStatus<
|
||||
ConnectorConfigType,
|
||||
ConnectorCredentialType
|
||||
>
|
||||
) => JSX.Element | string | undefined;
|
||||
}
|
||||
|
||||
interface ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType> {
|
||||
export interface ConnectorsTableProps<
|
||||
ConnectorConfigType,
|
||||
ConnectorCredentialType
|
||||
> {
|
||||
connectorIndexingStatuses: ConnectorIndexingStatus<
|
||||
ConnectorConfigType,
|
||||
ConnectorCredentialType
|
||||
@ -116,7 +125,11 @@ interface ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType> {
|
||||
) => JSX.Element | string;
|
||||
onUpdate: () => void;
|
||||
onCredentialLink?: (connectorId: number) => void;
|
||||
specialColumns?: ColumnSpecification<ConnectorConfigType>[];
|
||||
specialColumns?: ColumnSpecification<
|
||||
ConnectorConfigType,
|
||||
ConnectorCredentialType
|
||||
>[];
|
||||
includeName?: boolean;
|
||||
}
|
||||
|
||||
export function ConnectorsTable<ConnectorConfigType, ConnectorCredentialType>({
|
||||
@ -126,6 +139,7 @@ export function ConnectorsTable<ConnectorConfigType, ConnectorCredentialType>({
|
||||
specialColumns,
|
||||
onUpdate,
|
||||
onCredentialLink,
|
||||
includeName = false,
|
||||
}: ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType>) {
|
||||
const [popup, setPopup] = useState<{
|
||||
message: string;
|
||||
@ -136,6 +150,7 @@ export function ConnectorsTable<ConnectorConfigType, ConnectorCredentialType>({
|
||||
getCredential !== undefined && onCredentialLink !== undefined;
|
||||
|
||||
const columns = [
|
||||
...(includeName ? [{ header: "Name", key: "name" }] : []),
|
||||
...(specialColumns ?? []),
|
||||
{
|
||||
header: "Status",
|
||||
@ -202,10 +217,15 @@ export function ConnectorsTable<ConnectorConfigType, ConnectorCredentialType>({
|
||||
? Object.fromEntries(
|
||||
specialColumns.map(({ key, getValue }, i) => [
|
||||
key,
|
||||
getValue(connector),
|
||||
getValue(connectorIndexingStatus),
|
||||
])
|
||||
)
|
||||
: {}),
|
||||
...(includeName
|
||||
? {
|
||||
name: connectorIndexingStatus.name || "",
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
// index: (
|
||||
// <IndexButtonForTable
|
||||
|
@ -12,6 +12,7 @@ import { TrashIcon } from "@/components/icons/icons";
|
||||
import { updateConnector } from "@/lib/connector";
|
||||
import { AttachCredentialButtonForTable } from "@/components/admin/connectors/buttons/AttachCredentialButtonForTable";
|
||||
import { scheduleDeletionJobForConnector } from "@/lib/documentDeletion";
|
||||
import { ConnectorsTableProps } from "./ConnectorsTable";
|
||||
|
||||
const SingleUseConnectorStatus = ({
|
||||
indexingStatus,
|
||||
@ -43,26 +44,6 @@ const SingleUseConnectorStatus = ({
|
||||
return <div className="text-red-700">Failed</div>;
|
||||
};
|
||||
|
||||
interface ColumnSpecification<ConnectorConfigType> {
|
||||
header: string;
|
||||
key: string;
|
||||
getValue: (connector: Connector<ConnectorConfigType>) => JSX.Element | string;
|
||||
}
|
||||
|
||||
interface ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType> {
|
||||
connectorIndexingStatuses: ConnectorIndexingStatus<
|
||||
ConnectorConfigType,
|
||||
ConnectorCredentialType
|
||||
>[];
|
||||
liveCredential?: Credential<ConnectorCredentialType> | null;
|
||||
getCredential?: (
|
||||
credential: Credential<ConnectorCredentialType>
|
||||
) => JSX.Element | string;
|
||||
onUpdate: () => void;
|
||||
onCredentialLink?: (connectorId: number) => void;
|
||||
specialColumns?: ColumnSpecification<ConnectorConfigType>[];
|
||||
}
|
||||
|
||||
export function SingleUseConnectorsTable<
|
||||
ConnectorConfigType,
|
||||
ConnectorCredentialType
|
||||
@ -73,6 +54,7 @@ export function SingleUseConnectorsTable<
|
||||
specialColumns,
|
||||
onUpdate,
|
||||
onCredentialLink,
|
||||
includeName = false,
|
||||
}: ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType>) {
|
||||
const [popup, setPopup] = useState<{
|
||||
message: string;
|
||||
@ -181,7 +163,7 @@ export function SingleUseConnectorsTable<
|
||||
? Object.fromEntries(
|
||||
specialColumns.map(({ key, getValue }, i) => [
|
||||
key,
|
||||
getValue(connector),
|
||||
getValue(connectorIndexingStatus),
|
||||
])
|
||||
)
|
||||
: {}),
|
||||
|
@ -39,6 +39,7 @@ import Image from "next/image";
|
||||
import jiraSVG from "../../../public/Jira.svg";
|
||||
import confluenceSVG from "../../../public/Confluence.svg";
|
||||
import guruIcon from "../../../public/Guru.svg";
|
||||
import gongIcon from "../../../public/Gong.png";
|
||||
import zulipIcon from "../../../public/Zulip.png";
|
||||
import linearIcon from "../../../public/Linear.png";
|
||||
import hubSpotIcon from "../../../public/HubSpot.png";
|
||||
@ -423,6 +424,18 @@ export const GuruIcon = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
export const GongIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => (
|
||||
<div
|
||||
style={{ width: `${size}px`, height: `${size}px` }}
|
||||
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
>
|
||||
<Image src={gongIcon} alt="Logo" width="96" height="96" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const HubSpotIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
|
@ -23,6 +23,7 @@ const sources: Source[] = [
|
||||
{ displayName: "Github PRs", internalName: "github" },
|
||||
{ displayName: "Web", internalName: "web" },
|
||||
{ displayName: "Guru", internalName: "guru" },
|
||||
{ displayName: "Gong", internalName: "gong" },
|
||||
{ displayName: "File", internalName: "file" },
|
||||
{ displayName: "Notion", internalName: "notion" },
|
||||
{ displayName: "Zulip", internalName: "zulip" },
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
GlobeIcon,
|
||||
GoogleDriveIcon,
|
||||
GuruIcon,
|
||||
GongIcon,
|
||||
JiraIcon,
|
||||
LinearIcon,
|
||||
NotionIcon,
|
||||
@ -103,6 +104,12 @@ export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => {
|
||||
displayName: "Guru",
|
||||
adminPageLink: "/admin/connectors/guru",
|
||||
};
|
||||
case "gong":
|
||||
return {
|
||||
icon: GongIcon,
|
||||
displayName: "Gong",
|
||||
adminPageLink: "/admin/connectors/gong",
|
||||
};
|
||||
case "linear":
|
||||
return {
|
||||
icon: LinearIcon,
|
||||
|
@ -19,6 +19,7 @@ export type ValidSources =
|
||||
| "slab"
|
||||
| "notion"
|
||||
| "guru"
|
||||
| "gong"
|
||||
| "zulip"
|
||||
| "linear"
|
||||
| "hubspot"
|
||||
@ -96,6 +97,10 @@ export interface SlabConfig {
|
||||
|
||||
export interface GuruConfig {}
|
||||
|
||||
export interface GongConfig {
|
||||
workspaces?: string[];
|
||||
}
|
||||
|
||||
export interface FileConfig {
|
||||
file_locations: string[];
|
||||
}
|
||||
@ -203,6 +208,11 @@ export interface GuruCredentialJson {
|
||||
guru_user_token: string;
|
||||
}
|
||||
|
||||
export interface GongCredentialJson {
|
||||
gong_access_key: string;
|
||||
gong_access_key_secret: string;
|
||||
}
|
||||
|
||||
export interface LinearCredentialJson {
|
||||
linear_api_key: string;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user