From 73b063b66c79b24757ce1c36f21e9dcdebda2447 Mon Sep 17 00:00:00 2001 From: Hagenoneill Date: Thu, 29 Feb 2024 12:36:05 -0500 Subject: [PATCH 01/32] added teams connector --- backend/danswer/configs/constants.py | 1 + backend/danswer/connectors/factory.py | 2 + backend/danswer/connectors/teams/__init__.py | 0 backend/danswer/connectors/teams/connector.py | 223 +++++++++++++++ web/public/Teams.png | Bin 0 -> 126479 bytes web/src/app/admin/connectors/teams/page.tsx | 264 ++++++++++++++++++ web/src/components/icons/icons.tsx | 13 + web/src/lib/sources.ts | 6 + web/src/lib/types.ts | 11 + 9 files changed, 520 insertions(+) create mode 100644 backend/danswer/connectors/teams/__init__.py create mode 100644 backend/danswer/connectors/teams/connector.py create mode 100644 web/public/Teams.png create mode 100644 web/src/app/admin/connectors/teams/page.tsx diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index 0151634ed..76fc419c0 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -83,6 +83,7 @@ class DocumentSource(str, Enum): ZENDESK = "zendesk" LOOPIO = "loopio" SHAREPOINT = "sharepoint" + TEAMS = "teams" class DocumentIndexType(str, Enum): diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index f4a9ee290..4272de302 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -26,6 +26,7 @@ from danswer.connectors.notion.connector import NotionConnector from danswer.connectors.productboard.connector import ProductboardConnector from danswer.connectors.requesttracker.connector import RequestTrackerConnector from danswer.connectors.sharepoint.connector import SharepointConnector +from danswer.connectors.teams.connector import TeamsConnector from danswer.connectors.slab.connector import SlabConnector from danswer.connectors.slack.connector import SlackPollConnector from danswer.connectors.slack.load_connector import SlackLoadConnector @@ -70,6 +71,7 @@ def identify_connector_class( DocumentSource.ZENDESK: ZendeskConnector, DocumentSource.LOOPIO: LoopioConnector, DocumentSource.SHAREPOINT: SharepointConnector, + DocumentSource.TEAMS: TeamsConnector, } connector_by_source = connector_map.get(source, {}) diff --git a/backend/danswer/connectors/teams/__init__.py b/backend/danswer/connectors/teams/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py new file mode 100644 index 000000000..97495b1e5 --- /dev/null +++ b/backend/danswer/connectors/teams/connector.py @@ -0,0 +1,223 @@ +import io +import os +import tempfile +from datetime import datetime +from datetime import timezone +from typing import Any +from html.parser import HTMLParser + +import docx # type: ignore +import msal # type: ignore +import openpyxl # type: ignore +# import pptx # type: ignore +from office365.graph_client import GraphClient # type: ignore +from office365.teams.team import Team +from office365.teams.channels.channel import Channel +from office365.teams.chats.messages.message import ChatMessage +from office365.outlook.mail.item_body import ItemBody + +from danswer.configs.app_configs import INDEX_BATCH_SIZE +from danswer.configs.constants import DocumentSource +from danswer.connectors.cross_connector_utils.file_utils import is_text_file_extension +from danswer.connectors.cross_connector_utils.file_utils import read_pdf_file +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 BasicExpertInfo +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 + +UNSUPPORTED_FILE_TYPE_CONTENT = "" # idea copied from the google drive side of things + + +logger = setup_logger() + + +class HTMLFilter(HTMLParser): + text = "" + def handle_data(self, data): + self.text += data + +def get_created_datetime(obj: ChatMessage): + # Extract the 'createdDateTime' value from the 'properties' dictionary + created_datetime_str = obj.properties['createdDateTime'] + + # Convert the string to a datetime object + return datetime.strptime(created_datetime_str, '%Y-%m-%dT%H:%M:%S.%f%z') + + +class TeamsConnector(LoadConnector, PollConnector): + def __init__( + self, + batch_size: int = INDEX_BATCH_SIZE, + teams: list[str] = [], + ) -> None: + self.batch_size = batch_size + self.graph_client: GraphClient | None = None + self.requested_team_list: list[str] = teams + + def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: + aad_client_id = credentials["aad_client_id"] + aad_client_secret = credentials["aad_client_secret"] + aad_directory_id = credentials["aad_directory_id"] + + def _acquire_token_func() -> dict[str, Any]: + """ + Acquire token via MSAL + """ + authority_url = f"https://login.microsoftonline.com/{aad_directory_id}" + app = msal.ConfidentialClientApplication( + authority=authority_url, + client_id=aad_client_id, + client_credential=aad_client_secret, + ) + token = app.acquire_token_for_client( + scopes=["https://graph.microsoft.com/.default"] + ) + return token + + self.graph_client = GraphClient(_acquire_token_func) + return None + + def get_message_list_from_channel(self, channel_object: Channel) -> list[ChatMessage]: + message_list: list[ChatMessage] = [] + message_object_collection = channel_object.messages.get().execute_query() + message_list.extend(message_object_collection) + + return message_list + + def get_all_channels( + self, + team_object_list: list[Team], + start: datetime | None = None, + end: datetime | None = None, + ) -> list[Channel]: + filter_str = "" + if start is not None and end is not None: + filter_str = f"last_modified_datetime ge {start.isoformat()} and last_modified_datetime le {end.isoformat()}" + + channel_list: list[Channel] = [] + for team_object in team_object_list: + query = team_object.channels.get() + if filter_str: + query = query.filter(filter_str) + channel_objects = query.execute_query() + channel_list.extend(channel_objects) + + return channel_list + + def get_all_teams_objects(self) -> list[Team]: + team_object_list: list[Team] = [] + + teams_object = self.graph_client.teams.get().execute_query() + + if len(self.requested_team_list) > 0: + for requested_team in self.requested_team_list: + adjusted_request_string = requested_team.replace(" ", "") + for team_object in teams_object: + adjusted_team_string = team_object.display_name.replace(" ", "") + if adjusted_team_string == adjusted_request_string: + team_object_list.append(team_object) + else: + team_object_list.extend(teams_object) + + return team_object_list + + def _fetch_from_teams( + self, start: datetime | None = None, end: datetime | None = None + ) -> GenerateDocumentsOutput: + if self.graph_client is None: + raise ConnectorMissingCredentialError("Teams") + + team_object_list = self.get_all_teams_objects() + + channel_list = self.get_all_channels( + team_object_list=team_object_list, + start=start, + end=end, + ) + + # goes over channels, converts them into Document objects and then yields them in batches + doc_batch: list[Document] = [] + batch_count = 0 + for channel_object in channel_list: + doc_batch.append( + self.convert_channel_object_to_document(channel_object) + ) + + batch_count += 1 + if batch_count >= self.batch_size: + yield doc_batch + batch_count = 0 + doc_batch = [] + yield doc_batch + + def convert_channel_object_to_document( + self, + channel_object: Channel, + ) -> Document: + channel_text, most_recent_message_datetime = self.extract_channel_text_and_latest_datetime(channel_object) + channel_members = self.extract_channel_members(channel_object) + doc = Document( + id=channel_object.id, + sections=[Section(link=channel_object.web_url, text=channel_text)], + source=DocumentSource.TEAMS, + semantic_identifier=channel_object.properties["displayName"], + doc_updated_at=most_recent_message_datetime, + primary_owners=channel_members, + metadata={}, + ) + return doc + + def extract_channel_members(self, channel_object: Channel)->list[BasicExpertInfo]: + channel_members_list: list[BasicExpertInfo] = [] + member_objects = channel_object.members.get().execute_query() + for member_object in member_objects: + channel_members_list.append( + BasicExpertInfo(display_name=member_object.display_name) + ) + return channel_members_list + + def extract_channel_text_and_latest_datetime(self, channel_object: Channel): + message_list = self.get_message_list_from_channel(channel_object) + sorted_message_list = sorted(message_list, key=get_created_datetime, reverse=True) + most_recent_datetime: datetime | None = None + if sorted_message_list: + most_recent_message = sorted_message_list[0] + most_recent_datetime = datetime.strptime(most_recent_message.properties["createdDateTime"], + '%Y-%m-%dT%H:%M:%S.%f%z') + messages_text = "" + for message in message_list: + if message.body.content: + html_parser = HTMLFilter() + html_parser.feed(message.body.content) + messages_text += html_parser.text + + return messages_text, most_recent_datetime + + def load_from_state(self) -> GenerateDocumentsOutput: + return self._fetch_from_teams() + + def poll_source( + self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch + ) -> GenerateDocumentsOutput: + start_datetime = datetime.utcfromtimestamp(start) + end_datetime = datetime.utcfromtimestamp(end) + return self._fetch_from_teams(start=start_datetime, end=end_datetime) + + +if __name__ == "__main__": + connector = TeamsConnector(sites=os.environ["SITES"].split(",")) + + connector.load_credentials( + { + "aad_client_id": os.environ["AAD_CLIENT_ID"], + "aad_client_secret": os.environ["AAD_CLIENT_SECRET"], + "aad_directory_id": os.environ["AAD_CLIENT_DIRECTORY_ID"], + } + ) + document_batches = connector.load_from_state() + print(next(document_batches)) diff --git a/web/public/Teams.png b/web/public/Teams.png new file mode 100644 index 0000000000000000000000000000000000000000..c8479db6933c6693817c2a91ce63bb03894ab197 GIT binary patch literal 126479 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­f#&HzIayF+s(+V7}HrmgnxALhM9jTa9E8|9l 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 { + const { mutate } = useSWRConfig(); + const { + data: connectorIndexingStatuses, + isLoading: isConnectorIndexingStatusesLoading, + error: isConnectorIndexingStatusesError, + } = useSWR[]>( + "/api/manage/admin/connector/indexing-status", + fetcher + ); + + const { + data: credentialsData, + isLoading: isCredentialsLoading, + error: isCredentialsError, + refreshCredentials, + } = usePublicCredentials(); + + if ( + (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) || + (!credentialsData && isCredentialsLoading) + ) { + return ; + } + + if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { + return
Failed to load connectors
; + } + + if (isCredentialsError || !credentialsData) { + return
Failed to load credentials
; + } + + const teamsConnectorIndexingStatuses: ConnectorIndexingStatus< + TeamsConfig, + TeamsCredentialJson + >[] = connectorIndexingStatuses.filter( + (connectorIndexingStatus) => + connectorIndexingStatus.connector.source === "teams" + ); + + const teamsCredential: Credential | undefined = + credentialsData.find( + (credential) => credential.credential_json?.aad_client_id + ); + + return ( + <> + + The Teams connector allows you to index and search through your + Teams channels. Once setup, all messages from the channels contained + in the specified teams will be queryable within Danswer. + + + + Step 1: Provide Teams credentials + + {teamsCredential ? ( + <> +
+ Existing Azure AD Client ID: + + {teamsCredential.credential_json.aad_client_id} + + +
+ + ) : ( + <> + + As a first step, please provide Application (client) ID, Directory + (tenant) ID, and Client Secret. You can follow the guide{" "} + + here + {" "} + to create an Azure AD application and obtain these values. + + + + formBody={ + <> + + + + + } + validationSchema={Yup.object().shape({ + aad_client_id: Yup.string().required( + "Please enter your Application (client) ID" + ), + aad_directory_id: Yup.string().required( + "Please enter your Directory (tenant) ID" + ), + aad_client_secret: Yup.string().required( + "Please enter your Client Secret" + ), + })} + initialValues={{ + aad_client_id: "", + aad_directory_id: "", + aad_client_secret: "", + }} + onSubmit={(isSuccess) => { + if (isSuccess) { + refreshCredentials(); + } + }} + /> + + + )} + + + Step 2: Manage Teams Connector + + + {teamsConnectorIndexingStatuses.length > 0 && ( + <> + + The latest messages from the specified teams are + fetched every 10 minutes. + +
+ + connectorIndexingStatuses={teamsConnectorIndexingStatuses} + liveCredential={teamsCredential} + getCredential={(credential) => + credential.credential_json.aad_directory_id + } + onUpdate={() => + mutate("/api/manage/admin/connector/indexing-status") + } + onCredentialLink={async (connectorId) => { + if (teamsCredential) { + await linkCredential(connectorId, teamsCredential.id); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + specialColumns={[ + { + header: "Connectors", + key: "connectors", + getValue: (ccPairStatus) => { + const connectorConfig = + ccPairStatus.connector.connector_specific_config; + return `${connectorConfig.teams}`; + }, + }, + ]} + includeName + /> +
+ + )} + + {teamsCredential ? ( + + + nameBuilder={(values) => + values.teams && values.teams.length > 0 + ? `Teams-${values.teams.join("-")}` + : "Teams" + } + ccPairNameBuilder={(values) => + values.teams && values.teams.length > 0 + ? `Teams-${values.teams.join("-")}` + : "Teams" + } + source="teams" + inputType="poll" + // formBody={<>} + formBodyBuilder={TextArrayFieldBuilder({ + name: "teams", + label: "Teams:", + subtext: + "Specify 0 or more Teams to index. " + + "For example, specifying the Team 'Support' for the 'danswerai' Org will cause " + + "us to only index messages sent in channels belonging to the 'Support' Team. " + + "If no Teams are specified, all Teams in your organization will be indexed.", + })} + validationSchema={Yup.object().shape({ + teams: Yup.array() + .of(Yup.string().required("Team names must be strings")) + .required(), + })} + initialValues={{ + teams: [], + }} + credentialId={teamsCredential.id} + refreshFreq={10 * 60} // 10 minutes + /> + + ) : ( + + Please provide all Azure info in Step 1 first! Once you're done + with that, you can then specify which teams you want to + make searchable. + + )} + + ); +}; + +export default function Page() { + return ( +
+
+ +
+ + } title="Teams" /> + + +
+ ); +} diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index e5ae456cf..537d747db 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -51,6 +51,7 @@ import document360Icon from "../../../public/Document360.png"; import googleSitesIcon from "../../../public/GoogleSites.png"; import zendeskIcon from "../../../public/Zendesk.svg"; import sharepointIcon from "../../../public/Sharepoint.png"; +import teamsIcon from "../../../public/Teams.png"; import { FaRobot } from "react-icons/fa"; interface IconProps { @@ -526,6 +527,18 @@ export const SharepointIcon = ({ ); +export const TeamsIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => ( +
+ Logo +
+); + export const GongIcon = ({ size = 16, className = defaultTailwindCSS, diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index bcd821121..eb883cb4e 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -19,6 +19,7 @@ import { ProductboardIcon, RequestTrackerIcon, SharepointIcon, + TeamsIcon, SlabIcon, SlackIcon, ZendeskIcon, @@ -154,6 +155,11 @@ const SOURCE_METADATA_MAP: SourceMap = { displayName: "Sharepoint", category: SourceCategory.AppConnection, }, + teams: { + icon: TeamsIcon, + displayName: "Teams", + category: SourceCategory.AppConnection, + }, requesttracker: { icon: RequestTrackerIcon, displayName: "Request Tracker", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 373f409e7..106fb6d05 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -33,6 +33,7 @@ export type ValidSources = | "google_sites" | "loopio" | "sharepoint" + | "teams" | "zendesk"; export type ValidInputTypes = "load_state" | "poll" | "event"; @@ -110,6 +111,10 @@ export interface SharepointConfig { sites?: string[]; } +export interface TeamsConfig { + teams?: string[]; +} + export interface ProductboardConfig {} export interface SlackConfig { @@ -314,6 +319,12 @@ export interface SharepointCredentialJson { aad_directory_id: string; } +export interface TeamsCredentialJson { + aad_client_id: string; + aad_client_secret: string; + aad_directory_id: string; +} + // DELETION export interface DeletionAttemptSnapshot { From 51b4e63218fd3ed9af3a12c034b1b61b98b2636b Mon Sep 17 00:00:00 2001 From: Hagenoneill Date: Mon, 1 Apr 2024 14:31:26 -0400 Subject: [PATCH 02/32] organized documents by post instead of by channel --- backend/danswer/connectors/teams/connector.py | 104 ++++++++++++------ 1 file changed, 69 insertions(+), 35 deletions(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index 97495b1e5..c16ed5ad1 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -82,14 +82,22 @@ class TeamsConnector(LoadConnector, PollConnector): self.graph_client = GraphClient(_acquire_token_func) return None - def get_message_list_from_channel(self, channel_object: Channel) -> list[ChatMessage]: - message_list: list[ChatMessage] = [] - message_object_collection = channel_object.messages.get().execute_query() - message_list.extend(message_object_collection) - - return message_list + def get_post_message_lists_from_channel(self, channel_object: Channel) -> list[ChatMessage]: + + base_message_list: list[ChatMessage] = channel_object.messages.get().execute_query() - def get_all_channels( + post_message_lists: list[list[ChatMessage]] = [] + for message in base_message_list: + replies = message.replies.get().execute_query() + + post_message_list: list[ChatMessage] = [message] + post_message_list.extend(replies) + + post_message_lists.append(post_message_list) + + return post_message_lists + + def get_channel_object_list_from_team_list( self, team_object_list: list[Team], start: datetime | None = None, @@ -109,7 +117,7 @@ class TeamsConnector(LoadConnector, PollConnector): return channel_list - def get_all_teams_objects(self) -> list[Team]: + def get_all_team_objects(self) -> list[Team]: team_object_list: list[Team] = [] teams_object = self.graph_client.teams.get().execute_query() @@ -132,9 +140,9 @@ class TeamsConnector(LoadConnector, PollConnector): if self.graph_client is None: raise ConnectorMissingCredentialError("Teams") - team_object_list = self.get_all_teams_objects() + team_object_list = self.get_all_team_objects() - channel_list = self.get_all_channels( + channel_list = self.get_channel_object_list_from_team_list( team_object_list=team_object_list, start=start, end=end, @@ -144,8 +152,10 @@ class TeamsConnector(LoadConnector, PollConnector): doc_batch: list[Document] = [] batch_count = 0 for channel_object in channel_list: + post_message_lists = self.get_post_message_lists_from_channel(channel_object) doc_batch.append( - self.convert_channel_object_to_document(channel_object) + self.convert_post_message_list_to_document(channel_object, + post_message_lists) ) batch_count += 1 @@ -155,19 +165,60 @@ class TeamsConnector(LoadConnector, PollConnector): doc_batch = [] yield doc_batch - def convert_channel_object_to_document( + def convert_post_message_list_to_document( self, channel_object: Channel, + post_message_list: list[ChatMessage], ) -> Document: - channel_text, most_recent_message_datetime = self.extract_channel_text_and_latest_datetime(channel_object) - channel_members = self.extract_channel_members(channel_object) + most_recent_message_datetime: datetime | None = None + semantic_string: str = "Channel/Post: " + channel_object.properties["displayName"] + post_id: str = channel_object.id + web_url: str = channel_object.web_url + messages_text = "" + post_members_list: list[BasicExpertInfo] = [] + + sorted_post_message_list = sorted(post_message_list, key=get_created_datetime, reverse=True) + + if sorted_post_message_list: + most_recent_message = sorted_post_message_list[0] + most_recent_message_datetime = datetime.strptime(most_recent_message.properties["createdDateTime"], + '%Y-%m-%dT%H:%M:%S.%f%z') + + for message in post_message_list: + # add text and a newline + if message.body.content: + html_parser = HTMLFilter() + html_parser.feed(message.body.content) + messages_text += html_parser.text + '\n' + + # if it has a subject, that means its the top level post message, so grab its id, url, and subject + if message.properties['subject']: + semantic_string += "/" + message.properties["subject"] + post_id = message.properties["id"] + web_url = message.web_url + + # check to make sure there is a valid display name + if message.properties["from"]: + if message.properties["from"]["user"]: + if message.properties["from"]["user"]["displayName"]: + message_sender = message.properties["from"]["user"]["displayName"] + # if its not a duplicate, add it to the list + if message_sender not in [member.display_name for member in post_members_list]: + post_members_list.append( + BasicExpertInfo(display_name=message_sender) + ) + + # if there are no found post members, grab the members from the parent channel + if not post_members_list: + post_members_list = self.extract_channel_members(channel_object) + doc = Document( - id=channel_object.id, - sections=[Section(link=channel_object.web_url, text=channel_text)], + id=post_id, + sections=[Section(link=web_url, text=messages_text)], source=DocumentSource.TEAMS, - semantic_identifier=channel_object.properties["displayName"], + semantic_identifier=semantic_string, doc_updated_at=most_recent_message_datetime, - primary_owners=channel_members, + primary_owners=post_members_list, metadata={}, ) return doc @@ -180,23 +231,6 @@ class TeamsConnector(LoadConnector, PollConnector): BasicExpertInfo(display_name=member_object.display_name) ) return channel_members_list - - def extract_channel_text_and_latest_datetime(self, channel_object: Channel): - message_list = self.get_message_list_from_channel(channel_object) - sorted_message_list = sorted(message_list, key=get_created_datetime, reverse=True) - most_recent_datetime: datetime | None = None - if sorted_message_list: - most_recent_message = sorted_message_list[0] - most_recent_datetime = datetime.strptime(most_recent_message.properties["createdDateTime"], - '%Y-%m-%dT%H:%M:%S.%f%z') - messages_text = "" - for message in message_list: - if message.body.content: - html_parser = HTMLFilter() - html_parser.feed(message.body.content) - messages_text += html_parser.text - - return messages_text, most_recent_datetime def load_from_state(self) -> GenerateDocumentsOutput: return self._fetch_from_teams() From 818dfd041358d7d96ef1357d8ed612f49eced922 Mon Sep 17 00:00:00 2001 From: Hagenoneill Date: Mon, 1 Apr 2024 14:39:37 -0400 Subject: [PATCH 03/32] JUST get_all LOL --- backend/danswer/connectors/teams/connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index c16ed5ad1..7bab34202 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -88,7 +88,7 @@ class TeamsConnector(LoadConnector, PollConnector): post_message_lists: list[list[ChatMessage]] = [] for message in base_message_list: - replies = message.replies.get().execute_query() + replies = message.replies.get_all().execute_query() post_message_list: list[ChatMessage] = [message] post_message_list.extend(replies) From ea71b9830ca17353c550f4cf003f9305be2946ee Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Tue, 4 Jun 2024 12:11:19 -0400 Subject: [PATCH 04/32] Update types.ts removed semi --- web/src/lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 2b59cee70..9271c981f 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -44,7 +44,7 @@ export type ValidSources = | "loopio" | "sharepoint" | "teams" - | "zendesk"; + | "zendesk" | "discourse" | "axero" | "wikipedia" From 14a39e88e876e0f22c86831662f020869506a2d3 Mon Sep 17 00:00:00 2001 From: Hagen O'Neill Date: Tue, 4 Jun 2024 09:45:10 -0700 Subject: [PATCH 05/32] seperated teams and sharepoint enviornment variables --- .../connectors/sharepoint/connector.py | 18 +++++----- backend/danswer/connectors/teams/connector.py | 34 ++++++++++--------- .../app/admin/connectors/sharepoint/page.tsx | 24 ++++++------- web/src/app/admin/connectors/teams/page.tsx | 24 ++++++------- web/src/lib/types.ts | 12 +++---- 5 files changed, 57 insertions(+), 55 deletions(-) diff --git a/backend/danswer/connectors/sharepoint/connector.py b/backend/danswer/connectors/sharepoint/connector.py index 0c7497d0c..c1a888b59 100644 --- a/backend/danswer/connectors/sharepoint/connector.py +++ b/backend/danswer/connectors/sharepoint/connector.py @@ -70,19 +70,19 @@ class SharepointConnector(LoadConnector, PollConnector): self.requested_site_list: list[str] = sites def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: - aad_client_id = credentials["aad_client_id"] - aad_client_secret = credentials["aad_client_secret"] - aad_directory_id = credentials["aad_directory_id"] + sp_client_id = credentials["sp_client_id"] + sp_client_secret = credentials["sp_client_secret"] + sp_directory_id = credentials["sp_directory_id"] def _acquire_token_func() -> dict[str, Any]: """ Acquire token via MSAL """ - authority_url = f"https://login.microsoftonline.com/{aad_directory_id}" + authority_url = f"https://login.microsoftonline.com/{sp_directory_id}" app = msal.ConfidentialClientApplication( authority=authority_url, - client_id=aad_client_id, - client_credential=aad_client_secret, + client_id=sp_client_id, + client_credential=sp_client_secret, ) token = app.acquire_token_for_client( scopes=["https://graph.microsoft.com/.default"] @@ -224,9 +224,9 @@ if __name__ == "__main__": connector.load_credentials( { - "aad_client_id": os.environ["AAD_CLIENT_ID"], - "aad_client_secret": os.environ["AAD_CLIENT_SECRET"], - "aad_directory_id": os.environ["AAD_CLIENT_DIRECTORY_ID"], + "sp_client_id": os.environ["SP_CLIENT_ID"], + "sp_client_secret": os.environ["SP_CLIENT_SECRET"], + "sp_directory_id": os.environ["SP_CLIENT_DIRECTORY_ID"], } ) document_batches = connector.load_from_state() diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index 7bab34202..bbaadba28 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -11,15 +11,13 @@ import msal # type: ignore import openpyxl # type: ignore # import pptx # type: ignore from office365.graph_client import GraphClient # type: ignore -from office365.teams.team import Team -from office365.teams.channels.channel import Channel -from office365.teams.chats.messages.message import ChatMessage -from office365.outlook.mail.item_body import ItemBody +from office365.teams.team import Team # type: ignore +from office365.teams.channels.channel import Channel # type: ignore +from office365.teams.chats.messages.message import ChatMessage # type: ignore +from office365.outlook.mail.item_body import ItemBody # type: ignore from danswer.configs.app_configs import INDEX_BATCH_SIZE from danswer.configs.constants import DocumentSource -from danswer.connectors.cross_connector_utils.file_utils import is_text_file_extension -from danswer.connectors.cross_connector_utils.file_utils import read_pdf_file from danswer.connectors.interfaces import GenerateDocumentsOutput from danswer.connectors.interfaces import LoadConnector from danswer.connectors.interfaces import PollConnector @@ -50,6 +48,7 @@ def get_created_datetime(obj: ChatMessage): class TeamsConnector(LoadConnector, PollConnector): + def __init__( self, batch_size: int = INDEX_BATCH_SIZE, @@ -60,19 +59,19 @@ class TeamsConnector(LoadConnector, PollConnector): self.requested_team_list: list[str] = teams def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: - aad_client_id = credentials["aad_client_id"] - aad_client_secret = credentials["aad_client_secret"] - aad_directory_id = credentials["aad_directory_id"] + teams_client_id = credentials["teams_client_id"] + teams_client_secret = credentials["teams_client_secret"] + teams_directory_id = credentials["teams_directory_id"] def _acquire_token_func() -> dict[str, Any]: """ Acquire token via MSAL """ - authority_url = f"https://login.microsoftonline.com/{aad_directory_id}" + authority_url = f"https://login.microsoftonline.com/{teams_directory_id}" app = msal.ConfidentialClientApplication( authority=authority_url, - client_id=aad_client_id, - client_credential=aad_client_secret, + client_id=teams_client_id, + client_credential=teams_client_secret, ) token = app.acquire_token_for_client( scopes=["https://graph.microsoft.com/.default"] @@ -118,6 +117,9 @@ class TeamsConnector(LoadConnector, PollConnector): return channel_list def get_all_team_objects(self) -> list[Team]: + if self.graph_client is None: + raise ConnectorMissingCredentialError("Sharepoint") + team_object_list: list[Team] = [] teams_object = self.graph_client.teams.get().execute_query() @@ -244,13 +246,13 @@ class TeamsConnector(LoadConnector, PollConnector): if __name__ == "__main__": - connector = TeamsConnector(sites=os.environ["SITES"].split(",")) + connector = TeamsConnector(teams=os.environ["TEAMS"].split(",")) connector.load_credentials( { - "aad_client_id": os.environ["AAD_CLIENT_ID"], - "aad_client_secret": os.environ["AAD_CLIENT_SECRET"], - "aad_directory_id": os.environ["AAD_CLIENT_DIRECTORY_ID"], + "teams_client_id": os.environ["TEAMS_CLIENT_ID"], + "teams_client_secret": os.environ["TEAMS_CLIENT_SECRET"], + "teams_directory_id": os.environ["TEAMS_CLIENT_DIRECTORY_ID"], } ) document_batches = connector.load_from_state() diff --git a/web/src/app/admin/connectors/sharepoint/page.tsx b/web/src/app/admin/connectors/sharepoint/page.tsx index 662bf1a44..7dda6e1f7 100644 --- a/web/src/app/admin/connectors/sharepoint/page.tsx +++ b/web/src/app/admin/connectors/sharepoint/page.tsx @@ -67,7 +67,7 @@ const MainSection = () => { const sharepointCredential: Credential | undefined = credentialsData.find( - (credential) => credential.credential_json?.aad_client_id + (credential) => credential.credential_json?.sp_client_id ); return ( @@ -87,7 +87,7 @@ const MainSection = () => {
Existing Azure AD Client ID: - {sharepointCredential.credential_json.aad_client_id} + {sharepointCredential.credential_json.sp_client_id} +
+ + ) : ( + <> + + See the Dropbox connector{" "} + + setup guide + {" "} + on the Danswer docs to obtain a Dropbox token. + + + + formBody={ + <> + + + } + validationSchema={Yup.object().shape({ + dropbox_access_token: Yup.string().required( + "Please enter your Dropbox API token" + ), + })} + initialValues={{ + dropbox_access_token: "", + }} + onSubmit={(isSuccess) => { + if (isSuccess) { + refreshCredentials(); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + /> + + + )} + + {dropboxConnectorIndexingStatuses.length > 0 && ( + <> + + Dropbox indexing status + + + The latest article changes are fetched every 10 minutes. + +
+ + connectorIndexingStatuses={dropboxConnectorIndexingStatuses} + liveCredential={dropboxCredential} + onCredentialLink={async (connectorId) => { + if (dropboxCredential) { + await linkCredential(connectorId, dropboxCredential.id); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + onUpdate={() => + mutate("/api/manage/admin/connector/indexing-status") + } + /> +
+ + )} + + {dropboxCredential && dropboxConnectorIndexingStatuses.length === 0 && ( + <> + +

Create Connection

+

+ Press connect below to start the connection to your Dropbox + instance. +

+ + nameBuilder={(values) => `Dropbox`} + ccPairNameBuilder={(values) => `Dropbox`} + source="dropbox" + inputType="poll" + formBody={<>} + validationSchema={Yup.object().shape({})} + initialValues={{}} + refreshFreq={10 * 60} // 10 minutes + credentialId={dropboxCredential.id} + /> +
+ + )} + + ); +}; + +export default function Page() { + return ( +
+
+ +
+ } title="Dropbox" /> +
+
+ ); +} diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 8cbec72c4..04d003a59 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -51,6 +51,7 @@ import hubSpotIcon from "../../../public/HubSpot.png"; import document360Icon from "../../../public/Document360.png"; import googleSitesIcon from "../../../public/GoogleSites.png"; import zendeskIcon from "../../../public/Zendesk.svg"; +import dropboxIcon from "../../../public/Dropbox.png"; import sharepointIcon from "../../../public/Sharepoint.png"; import teamsIcon from "../../../public/Teams.png"; import mediawikiIcon from "../../../public/MediaWiki.svg"; @@ -617,6 +618,18 @@ export const ZendeskIcon = ({ ); +export const DropboxIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => ( +
+ Logo +
+); + export const DiscourseIcon = ({ size = 16, className = defaultTailwindCSS, diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index 303108394..597f45f4d 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -4,6 +4,7 @@ import { ConfluenceIcon, DiscourseIcon, Document360Icon, + DropboxIcon, FileIcon, GithubIcon, GitlabIcon, @@ -154,6 +155,11 @@ const SOURCE_METADATA_MAP: SourceMap = { displayName: "Loopio", category: SourceCategory.AppConnection, }, + dropbox: { + icon: DropboxIcon, + displayName: "Dropbox", + category: SourceCategory.AppConnection, + }, sharepoint: { icon: SharepointIcon, displayName: "Sharepoint", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 55cfe700c..f20173d1d 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -42,6 +42,7 @@ export type ValidSources = | "file" | "google_sites" | "loopio" + | "dropbox" | "sharepoint" | "teams" | "zendesk" @@ -191,6 +192,8 @@ export interface GoogleSitesConfig { export interface ZendeskConfig {} +export interface DropboxConfig {} + export interface MediaWikiBaseConfig { connector_name: string; language_code: string; @@ -198,6 +201,7 @@ export interface MediaWikiBaseConfig { pages?: string[]; recurse_depth?: number; } + export interface MediaWikiConfig extends MediaWikiBaseConfig { hostname: string; } @@ -362,6 +366,10 @@ export interface ZendeskCredentialJson { zendesk_token: string; } +export interface DropboxCredentialJson { + dropbox_access_token: string; +} + export interface SharepointCredentialJson { sp_client_id: string; sp_client_secret: string; From a2349af65c4ea42299b33f8612ac07e80374a511 Mon Sep 17 00:00:00 2001 From: Hagenoneill Date: Thu, 29 Feb 2024 12:36:05 -0500 Subject: [PATCH 16/32] added teams connector --- backend/danswer/configs/constants.py | 1 + backend/danswer/connectors/factory.py | 1 + web/src/components/icons/icons.tsx | 1 + web/src/lib/sources.ts | 5 +++++ web/src/lib/types.ts | 10 ++++++++++ 5 files changed, 18 insertions(+) diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index 58a782541..73013b88e 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -100,6 +100,7 @@ class DocumentSource(str, Enum): AXERO = "axero" MEDIAWIKI = "mediawiki" WIKIPEDIA = "wikipedia" + TEAMS = "teams" class DocumentIndexType(str, Enum): diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index 37ecd8f59..1b7a203f2 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -82,6 +82,7 @@ def identify_connector_class( DocumentSource.AXERO: AxeroConnector, DocumentSource.MEDIAWIKI: MediaWikiConnector, DocumentSource.WIKIPEDIA: WikipediaConnector, + DocumentSource.TEAMS: TeamsConnector, } connector_by_source = connector_map.get(source, {}) diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 04d003a59..a7b407a85 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -57,6 +57,7 @@ import teamsIcon from "../../../public/Teams.png"; import mediawikiIcon from "../../../public/MediaWiki.svg"; import wikipediaIcon from "../../../public/Wikipedia.svg"; import discourseIcon from "../../../public/Discourse.png"; +import teamsIcon from "../../../public/Teams.png"; import { FaRobot } from "react-icons/fa"; interface IconProps { diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index 597f45f4d..63717f196 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -190,6 +190,11 @@ const SOURCE_METADATA_MAP: SourceMap = { displayName: "MediaWiki", category: SourceCategory.AppConnection, }, + teams: { + icon: TeamsIcon, + displayName: "Teams", + category: SourceCategory.AppConnection, + }, requesttracker: { icon: RequestTrackerIcon, displayName: "Request Tracker", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index f20173d1d..dd37a27a5 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -141,6 +141,10 @@ export interface AxeroConfig { spaces?: string[]; } +export interface TeamsConfig { + teams?: string[]; +} + export interface ProductboardConfig {} export interface SlackConfig { @@ -395,6 +399,12 @@ export interface AxeroCredentialJson { export interface MediaWikiCredentialJson {} export interface WikipediaCredentialJson extends MediaWikiCredentialJson {} +export interface TeamsCredentialJson { + aad_client_id: string; + aad_client_secret: string; + aad_directory_id: string; +} + // DELETION export interface DeletionAttemptSnapshot { From f34b26b3d065e67f61f5725c10b52f8e41b2620f Mon Sep 17 00:00:00 2001 From: Hagen O'Neill Date: Tue, 4 Jun 2024 09:45:10 -0700 Subject: [PATCH 17/32] seperated teams and sharepoint enviornment variables --- backend/danswer/connectors/teams/connector.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index 15869c3ca..c26f1b5a0 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -42,6 +42,7 @@ def get_created_datetime(obj: ChatMessage) -> datetime: class TeamsConnector(LoadConnector, PollConnector): + def __init__( self, batch_size: int = INDEX_BATCH_SIZE, From 713d325f428c1d9f306a9cba6c304aeda7a78bd7 Mon Sep 17 00:00:00 2001 From: Hagen O'Neill Date: Tue, 4 Jun 2024 21:25:16 -0700 Subject: [PATCH 18/32] fixed rebase issues --- backend/danswer/configs/constants.py | 1 - backend/danswer/connectors/factory.py | 3 +-- web/src/components/icons/icons.tsx | 1 - web/src/lib/sources.ts | 5 ----- 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index 73013b88e..58a782541 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -100,7 +100,6 @@ class DocumentSource(str, Enum): AXERO = "axero" MEDIAWIKI = "mediawiki" WIKIPEDIA = "wikipedia" - TEAMS = "teams" class DocumentIndexType(str, Enum): diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index 1b7a203f2..41072eb74 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -30,10 +30,10 @@ from danswer.connectors.notion.connector import NotionConnector from danswer.connectors.productboard.connector import ProductboardConnector from danswer.connectors.requesttracker.connector import RequestTrackerConnector from danswer.connectors.sharepoint.connector import SharepointConnector -from danswer.connectors.teams.connector import TeamsConnector from danswer.connectors.slab.connector import SlabConnector from danswer.connectors.slack.connector import SlackPollConnector from danswer.connectors.slack.load_connector import SlackLoadConnector +from danswer.connectors.teams.connector import TeamsConnector from danswer.connectors.web.connector import WebConnector from danswer.connectors.wikipedia.connector import WikipediaConnector from danswer.connectors.zendesk.connector import ZendeskConnector @@ -82,7 +82,6 @@ def identify_connector_class( DocumentSource.AXERO: AxeroConnector, DocumentSource.MEDIAWIKI: MediaWikiConnector, DocumentSource.WIKIPEDIA: WikipediaConnector, - DocumentSource.TEAMS: TeamsConnector, } connector_by_source = connector_map.get(source, {}) diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index a7b407a85..04d003a59 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -57,7 +57,6 @@ import teamsIcon from "../../../public/Teams.png"; import mediawikiIcon from "../../../public/MediaWiki.svg"; import wikipediaIcon from "../../../public/Wikipedia.svg"; import discourseIcon from "../../../public/Discourse.png"; -import teamsIcon from "../../../public/Teams.png"; import { FaRobot } from "react-icons/fa"; interface IconProps { diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index 63717f196..597f45f4d 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -190,11 +190,6 @@ const SOURCE_METADATA_MAP: SourceMap = { displayName: "MediaWiki", category: SourceCategory.AppConnection, }, - teams: { - icon: TeamsIcon, - displayName: "Teams", - category: SourceCategory.AppConnection, - }, requesttracker: { icon: RequestTrackerIcon, displayName: "Request Tracker", From 8d741763488d9a50b53ee40f1b0af0e1b789a05b Mon Sep 17 00:00:00 2001 From: Hagen O'Neill Date: Wed, 5 Jun 2024 14:11:38 -0700 Subject: [PATCH 19/32] completed code revision suggestions --- backend/danswer/connectors/teams/connector.py | 207 +++++++++--------- 1 file changed, 105 insertions(+), 102 deletions(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index c26f1b5a0..ba41bdff2 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -1,6 +1,6 @@ import os from datetime import datetime -from html.parser import HTMLParser +from datetime import timezone from typing import Any import msal # type: ignore @@ -19,30 +19,20 @@ from danswer.connectors.models import BasicExpertInfo from danswer.connectors.models import ConnectorMissingCredentialError from danswer.connectors.models import Document from danswer.connectors.models import Section +from danswer.file_processing.html_utils import parse_html_page_basic from danswer.utils.logger import setup_logger -# import pptx # type: ignore - logger = setup_logger() - -class HTMLFilter(HTMLParser): - text = "" - - def handle_data(self, data: str) -> None: - self.text += data +datetime_format_string = "%Y-%m-%dT%H:%M:%S.%f%z" def get_created_datetime(obj: ChatMessage) -> datetime: - # Extract the 'createdDateTime' value from the 'properties' dictionary - created_datetime_str = obj.properties["createdDateTime"] - - # Convert the string to a datetime object - return datetime.strptime(created_datetime_str, "%Y-%m-%dT%H:%M:%S.%f%z") + # Extract the 'createdDateTime' value from the 'properties' dictionary and convert it to a datetime object + return datetime.strptime(obj.properties["createdDateTime"], datetime_format_string) class TeamsConnector(LoadConnector, PollConnector): - def __init__( self, batch_size: int = INDEX_BATCH_SIZE, @@ -75,63 +65,74 @@ class TeamsConnector(LoadConnector, PollConnector): self.graph_client = GraphClient(_acquire_token_func) return None - def get_post_message_lists_from_channel( - self, channel_object: Channel - ) -> list[list[ChatMessage]]: - base_message_list: list[ - ChatMessage - ] = channel_object.messages.get().execute_query() - - post_message_lists: list[list[ChatMessage]] = [] - for message in base_message_list: - replies = message.replies.get_all().execute_query() - - post_message_list: list[ChatMessage] = [message] - post_message_list.extend(replies) - - post_message_lists.append(post_message_list) - - return post_message_lists - - def get_channel_object_list_from_team_list( + def _get_threads_from_channel( self, - team_object_list: list[Team], + channel: Channel, start: datetime | None = None, end: datetime | None = None, + ) -> list[list[ChatMessage]]: + # Ensure start and end are timezone-aware + if start and start.tzinfo is None: + start = start.replace(tzinfo=timezone.utc) + if end and end.tzinfo is None: + end = end.replace(tzinfo=timezone.utc) + + query = channel.messages.get() + base_messages: list[ChatMessage] = query.execute_query() + + threads: list[list[ChatMessage]] = [] + for base_message in base_messages: + message_datetime = datetime.strptime( + base_message.properties["lastModifiedDateTime"], datetime_format_string + ) + + if start and message_datetime < start: + continue + if end and message_datetime > end: + continue + + reply_query = base_message.replies.get_all() + replies = reply_query.execute_query() + + # start a list containing the base message and its replies + thread: list[ChatMessage] = [base_message] + thread.extend(replies) + + threads.append(thread) + + return threads + + def _get_channels_from_teams( + self, + teams: list[Team], ) -> list[Channel]: - filter_str = "" - if start is not None and end is not None: - filter_str = f"last_modified_datetime ge {start.isoformat()} and last_modified_datetime le {end.isoformat()}" + channels: list[Channel] = [] + for team in teams: + query = team.channels.get() + channels = query.execute_query() + channels.extend(channels) - channel_list: list[Channel] = [] - for team_object in team_object_list: - query = team_object.channels.get() - if filter_str: - query = query.filter(filter_str) - channel_objects = query.execute_query() - channel_list.extend(channel_objects) + return channels - return channel_list - - def get_all_team_objects(self) -> list[Team]: + def _get_all_teams(self) -> list[Team]: if self.graph_client is None: raise ConnectorMissingCredentialError("Teams") - team_object_list: list[Team] = [] + teams: list[Team] = [] - teams_object = self.graph_client.teams.get().execute_query() + teams = self.graph_client.teams.get().execute_query() if len(self.requested_team_list) > 0: for requested_team in self.requested_team_list: adjusted_request_string = requested_team.replace(" ", "") - for team_object in teams_object: - adjusted_team_string = team_object.display_name.replace(" ", "") + for team in teams: + adjusted_team_string = team.display_name.replace(" ", "") if adjusted_team_string == adjusted_request_string: - team_object_list.append(team_object) + teams.append(team) else: - team_object_list.extend(teams_object) + teams.extend(teams) - return team_object_list + return teams def _fetch_from_teams( self, start: datetime | None = None, end: datetime | None = None @@ -139,72 +140,57 @@ class TeamsConnector(LoadConnector, PollConnector): if self.graph_client is None: raise ConnectorMissingCredentialError("Teams") - team_object_list = self.get_all_team_objects() + teams = self._get_all_teams() - channel_list = self.get_channel_object_list_from_team_list( - team_object_list=team_object_list, - start=start, - end=end, + channels = self._get_channels_from_teams( + teams=teams, ) # goes over channels, converts them into Document objects and then yields them in batches doc_batch: list[Document] = [] - batch_count = 0 - for channel_object in channel_list: - post_message_lists = self.get_post_message_lists_from_channel( - channel_object - ) - for base_message_groups in post_message_lists: - doc_batch.append( - self.convert_post_message_list_to_document( - channel_object, base_message_groups - ) - ) + for channel in channels: + thread_list = self._get_threads_from_channel(channel, start=start, end=end) + for thread in thread_list: + converted_doc = self._convert_thread_to_document(channel, thread) + if converted_doc: + doc_batch.append(converted_doc) - batch_count += 1 - if batch_count >= self.batch_size: + if len(doc_batch) >= self.batch_size: yield doc_batch - batch_count = 0 doc_batch = [] yield doc_batch - def convert_post_message_list_to_document( + def _convert_thread_to_document( self, - channel_object: Channel, - post_message_list: list[ChatMessage], - ) -> Document: + channel: Channel, + thread: list[ChatMessage], + ) -> Document | None: + if len(thread) <= 0: + return None + most_recent_message_datetime: datetime | None = None - semantic_string: str = ( - "Channel/Post: " + channel_object.properties["displayName"] - ) - post_id: str = channel_object.id - web_url: str = channel_object.web_url - messages_text = "" + top_message = thread[0] post_members_list: list[BasicExpertInfo] = [] + messages_text = "" - sorted_post_message_list = sorted( - post_message_list, key=get_created_datetime, reverse=True - ) + sorted_thread = sorted(thread, key=get_created_datetime, reverse=True) - if sorted_post_message_list: - most_recent_message = sorted_post_message_list[0] + if sorted_thread: + most_recent_message = sorted_thread[0] most_recent_message_datetime = datetime.strptime( most_recent_message.properties["createdDateTime"], - "%Y-%m-%dT%H:%M:%S.%f%z", + datetime_format_string, ) - for message in post_message_list: + for message in thread: # add text and a newline if message.body.content: - html_parser = HTMLFilter() - html_parser.feed(message.body.content) - messages_text += html_parser.text + "\n" + message_text = parse_html_page_basic(message.body.content) + messages_text += message_text # if it has a subject, that means its the top level post message, so grab its id, url, and subject if message.properties["subject"]: - semantic_string += "/" + message.properties["subject"] - post_id = message.properties["id"] - web_url = message.web_url + top_message = message # check to make sure there is a valid display name if message.properties["from"]: @@ -223,7 +209,24 @@ class TeamsConnector(LoadConnector, PollConnector): # if there are no found post members, grab the members from the parent channel if not post_members_list: - post_members_list = self.extract_channel_members(channel_object) + post_members_list = self._extract_channel_members(channel) + + semantic_string: str = "Post: " + channel.properties["displayName"] + + first_poster = top_message.properties["from"]["user"]["displayName"] + channel_name = channel.properties["displayName"] + thread_subject = top_message.properties["subject"] + snippet = parse_html_page_basic( + top_message.body.content[:50].rstrip() + "..." + if len(top_message.body.content) > 50 + else top_message.body.content + ) + if post_members_list: + semantic_string = ( + f"{first_poster} in {channel_name} about {thread_subject}: {snippet}" + ) + post_id = top_message.properties["id"] + web_url = top_message.web_url doc = Document( id=post_id, @@ -236,12 +239,12 @@ class TeamsConnector(LoadConnector, PollConnector): ) return doc - def extract_channel_members(self, channel_object: Channel) -> list[BasicExpertInfo]: + def _extract_channel_members(self, channel: Channel) -> list[BasicExpertInfo]: channel_members_list: list[BasicExpertInfo] = [] - member_objects = channel_object.members.get().execute_query() - for member_object in member_objects: + members = channel.members.get().execute_query() + for member in members: channel_members_list.append( - BasicExpertInfo(display_name=member_object.display_name) + BasicExpertInfo(display_name=member.display_name) ) return channel_members_list From 7b36f7aa4fddb049425c2645b733be3ed9991045 Mon Sep 17 00:00:00 2001 From: Hagen O'Neill Date: Wed, 5 Jun 2024 14:18:28 -0700 Subject: [PATCH 20/32] chat_message: ChatMessage --- backend/danswer/connectors/teams/connector.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index ba41bdff2..de81d3fc1 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -27,9 +27,11 @@ logger = setup_logger() datetime_format_string = "%Y-%m-%dT%H:%M:%S.%f%z" -def get_created_datetime(obj: ChatMessage) -> datetime: +def get_created_datetime(chat_message: ChatMessage) -> datetime: # Extract the 'createdDateTime' value from the 'properties' dictionary and convert it to a datetime object - return datetime.strptime(obj.properties["createdDateTime"], datetime_format_string) + return datetime.strptime( + chat_message.properties["createdDateTime"], datetime_format_string + ) class TeamsConnector(LoadConnector, PollConnector): From 9a9a879aee00f14e2d80159bbb003938ab855bb0 Mon Sep 17 00:00:00 2001 From: Hagen O'Neill Date: Wed, 5 Jun 2024 14:30:34 -0700 Subject: [PATCH 21/32] bugfixes --- backend/danswer/connectors/teams/connector.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index de81d3fc1..aff61aa69 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -108,19 +108,19 @@ class TeamsConnector(LoadConnector, PollConnector): self, teams: list[Team], ) -> list[Channel]: - channels: list[Channel] = [] + channels_list: list[Channel] = [] for team in teams: query = team.channels.get() channels = query.execute_query() - channels.extend(channels) + channels_list.extend(channels) - return channels + return channels_list def _get_all_teams(self) -> list[Team]: if self.graph_client is None: raise ConnectorMissingCredentialError("Teams") - teams: list[Team] = [] + teams_list: list[Team] = [] teams = self.graph_client.teams.get().execute_query() @@ -130,11 +130,11 @@ class TeamsConnector(LoadConnector, PollConnector): for team in teams: adjusted_team_string = team.display_name.replace(" ", "") if adjusted_team_string == adjusted_request_string: - teams.append(team) + teams_list.append(team) else: - teams.extend(teams) + teams_list.extend(teams) - return teams + return teams_list def _fetch_from_teams( self, start: datetime | None = None, end: datetime | None = None From 785d7736ed43fafe26c3b5d0ba51701ba83964ac Mon Sep 17 00:00:00 2001 From: Hagen O'Neill Date: Wed, 5 Jun 2024 14:37:53 -0700 Subject: [PATCH 22/32] extracted semantic identifier into its own method --- backend/danswer/connectors/teams/connector.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index aff61aa69..3362c1963 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -162,6 +162,20 @@ class TeamsConnector(LoadConnector, PollConnector): doc_batch = [] yield doc_batch + def _construct_semantic_identifier( + self, channel: Channel, top_message: ChatMessage + ) -> str: + first_poster = top_message.properties["from"]["user"]["displayName"] + channel_name = channel.properties["displayName"] + thread_subject = top_message.properties["subject"] + snippet = parse_html_page_basic( + top_message.body.content[:50].rstrip() + "..." + if len(top_message.body.content) > 50 + else top_message.body.content + ) + + return f"{first_poster} in {channel_name} about {thread_subject}: {snippet}" + def _convert_thread_to_document( self, channel: Channel, @@ -213,20 +227,7 @@ class TeamsConnector(LoadConnector, PollConnector): if not post_members_list: post_members_list = self._extract_channel_members(channel) - semantic_string: str = "Post: " + channel.properties["displayName"] - - first_poster = top_message.properties["from"]["user"]["displayName"] - channel_name = channel.properties["displayName"] - thread_subject = top_message.properties["subject"] - snippet = parse_html_page_basic( - top_message.body.content[:50].rstrip() + "..." - if len(top_message.body.content) > 50 - else top_message.body.content - ) - if post_members_list: - semantic_string = ( - f"{first_poster} in {channel_name} about {thread_subject}: {snippet}" - ) + semantic_string = self._construct_semantic_identifier(channel, top_message) post_id = top_message.properties["id"] web_url = top_message.web_url From 0b83396c4d0b605f17550d6cd2433fc39bdf19be Mon Sep 17 00:00:00 2001 From: Hagen O'Neill Date: Wed, 5 Jun 2024 15:14:08 -0700 Subject: [PATCH 23/32] disabled dropbox polling --- web/src/app/admin/connectors/dropbox/page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/app/admin/connectors/dropbox/page.tsx b/web/src/app/admin/connectors/dropbox/page.tsx index a42c8d141..6e8c103cc 100644 --- a/web/src/app/admin/connectors/dropbox/page.tsx +++ b/web/src/app/admin/connectors/dropbox/page.tsx @@ -150,7 +150,9 @@ const Main = () => { Dropbox indexing status - The latest article changes are fetched every 10 minutes. + Due to the short term access key, the Dropbox connector will only + index files after a new access key is provided and the indexing + process is re-run manually.
@@ -186,7 +188,7 @@ const Main = () => { formBody={<>} validationSchema={Yup.object().shape({})} initialValues={{}} - refreshFreq={10 * 60} // 10 minutes + // refreshFreq={10 * 60} // Disable polling credentialId={dropboxCredential.id} /> From 26bc785625f06f9b640d0bdb9f198743d5f8af6f Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Fri, 7 Jun 2024 10:53:15 -0700 Subject: [PATCH 24/32] Revert "Update README.md with fixed Slack link round 2" This reverts commit 0b6e85c26baf293c72a3fcf5fbec4a14d1a8128d. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 658ee6823..edd8328c3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Documentation - + Slack From 58c305a53948ba7f9e83f02e1a0e80738a41c6f0 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Fri, 7 Jun 2024 10:58:37 -0700 Subject: [PATCH 25/32] Revert "Add Dropbox connector (#956)" This reverts commit 914dc27a8f7f5255cae1624fc930d23621d52221. --- backend/danswer/configs/constants.py | 1 - .../danswer/connectors/dropbox/__init__.py | 0 .../danswer/connectors/dropbox/connector.py | 151 ------------- backend/danswer/connectors/factory.py | 2 - backend/requirements/default.txt | 1 - web/public/Dropbox.png | Bin 43377 -> 0 bytes web/src/app/admin/connectors/dropbox/page.tsx | 211 ------------------ web/src/components/icons/icons.tsx | 13 -- web/src/lib/sources.ts | 6 - web/src/lib/types.ts | 10 +- 10 files changed, 1 insertion(+), 394 deletions(-) delete mode 100644 backend/danswer/connectors/dropbox/__init__.py delete mode 100644 backend/danswer/connectors/dropbox/connector.py delete mode 100644 web/public/Dropbox.png delete mode 100644 web/src/app/admin/connectors/dropbox/page.tsx diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index 58a782541..641738a4c 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -93,7 +93,6 @@ class DocumentSource(str, Enum): GOOGLE_SITES = "google_sites" ZENDESK = "zendesk" LOOPIO = "loopio" - DROPBOX = "dropbox" SHAREPOINT = "sharepoint" TEAMS = "teams" DISCOURSE = "discourse" diff --git a/backend/danswer/connectors/dropbox/__init__.py b/backend/danswer/connectors/dropbox/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/danswer/connectors/dropbox/connector.py b/backend/danswer/connectors/dropbox/connector.py deleted file mode 100644 index 2fd39948a..000000000 --- a/backend/danswer/connectors/dropbox/connector.py +++ /dev/null @@ -1,151 +0,0 @@ -from datetime import timezone -from io import BytesIO -from typing import Any - -from dropbox import Dropbox # type: ignore -from dropbox.exceptions import ApiError # type:ignore -from dropbox.files import FileMetadata # type:ignore -from dropbox.files import FolderMetadata # type:ignore - -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.file_processing.extract_file_text import extract_file_text -from danswer.utils.logger import setup_logger - - -logger = setup_logger() - - -class DropboxConnector(LoadConnector, PollConnector): - def __init__(self, batch_size: int = INDEX_BATCH_SIZE) -> None: - self.batch_size = batch_size - self.dropbox_client: Dropbox | None = None - - def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: - self.dropbox_client = Dropbox(credentials["dropbox_access_token"]) - return None - - def _download_file(self, path: str) -> bytes: - """Download a single file from Dropbox.""" - if self.dropbox_client is None: - raise ConnectorMissingCredentialError("Dropbox") - _, resp = self.dropbox_client.files_download(path) - return resp.content - - def _get_shared_link(self, path: str) -> str: - """Create a shared link for a file in Dropbox.""" - if self.dropbox_client is None: - raise ConnectorMissingCredentialError("Dropbox") - - try: - # Check if a shared link already exists - shared_links = self.dropbox_client.sharing_list_shared_links(path=path) - if shared_links.links: - return shared_links.links[0].url - - link_metadata = ( - self.dropbox_client.sharing_create_shared_link_with_settings(path) - ) - return link_metadata.url - except ApiError as err: - logger.exception(f"Failed to create a shared link for {path}: {err}") - return "" - - def _yield_files_recursive( - self, - path: str, - start: SecondsSinceUnixEpoch | None, - end: SecondsSinceUnixEpoch | None, - ) -> GenerateDocumentsOutput: - """Yield files in batches from a specified Dropbox folder, including subfolders.""" - if self.dropbox_client is None: - raise ConnectorMissingCredentialError("Dropbox") - - result = self.dropbox_client.files_list_folder( - path, - limit=self.batch_size, - recursive=False, - include_non_downloadable_files=False, - ) - - while True: - batch: list[Document] = [] - for entry in result.entries: - if isinstance(entry, FileMetadata): - modified_time = entry.client_modified - if modified_time.tzinfo is None: - # If no timezone info, assume it is UTC - modified_time = modified_time.replace(tzinfo=timezone.utc) - else: - # If not in UTC, translate it - modified_time = modified_time.astimezone(timezone.utc) - - time_as_seconds = int(modified_time.timestamp()) - if start and time_as_seconds < start: - continue - if end and time_as_seconds > end: - continue - - downloaded_file = self._download_file(entry.path_display) - link = self._get_shared_link(entry.path_display) - try: - text = extract_file_text(entry.name, BytesIO(downloaded_file)) - batch.append( - Document( - id=f"doc:{entry.id}", - sections=[Section(link=link, text=text)], - source=DocumentSource.DROPBOX, - semantic_identifier=entry.name, - doc_updated_at=modified_time, - metadata={"type": "article"}, - ) - ) - except Exception as e: - logger.exception( - f"Error decoding file {entry.path_display} as utf-8 error occurred: {e}" - ) - - elif isinstance(entry, FolderMetadata): - yield from self._yield_files_recursive(entry.path_lower, start, end) - - if batch: - yield batch - - if not result.has_more: - break - - result = self.dropbox_client.files_list_folder_continue(result.cursor) - - def load_from_state(self) -> GenerateDocumentsOutput: - return self.poll_source(None, None) - - def poll_source( - self, start: SecondsSinceUnixEpoch | None, end: SecondsSinceUnixEpoch | None - ) -> GenerateDocumentsOutput: - if self.dropbox_client is None: - raise ConnectorMissingCredentialError("Dropbox") - - for batch in self._yield_files_recursive("", start, end): - yield batch - - return None - - -if __name__ == "__main__": - import os - - connector = DropboxConnector() - connector.load_credentials( - { - "dropbox_access_token": os.environ["DROPBOX_ACCESS_TOKEN"], - } - ) - document_batches = connector.load_from_state() - print(next(document_batches)) diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index 41072eb74..ee5e710f1 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -8,7 +8,6 @@ from danswer.connectors.confluence.connector import ConfluenceConnector from danswer.connectors.danswer_jira.connector import JiraConnector from danswer.connectors.discourse.connector import DiscourseConnector from danswer.connectors.document360.connector import Document360Connector -from danswer.connectors.dropbox.connector import DropboxConnector from danswer.connectors.file.connector import LocalFileConnector from danswer.connectors.github.connector import GithubConnector from danswer.connectors.gitlab.connector import GitlabConnector @@ -75,7 +74,6 @@ def identify_connector_class( DocumentSource.GOOGLE_SITES: GoogleSitesConnector, DocumentSource.ZENDESK: ZendeskConnector, DocumentSource.LOOPIO: LoopioConnector, - DocumentSource.DROPBOX: DropboxConnector, DocumentSource.SHAREPOINT: SharepointConnector, DocumentSource.TEAMS: TeamsConnector, DocumentSource.DISCOURSE: DiscourseConnector, diff --git a/backend/requirements/default.txt b/backend/requirements/default.txt index e25f02b7a..6052624ad 100644 --- a/backend/requirements/default.txt +++ b/backend/requirements/default.txt @@ -69,4 +69,3 @@ uvicorn==0.21.1 zulip==0.8.2 hubspot-api-client==8.1.0 zenpy==2.0.41 -dropbox==11.36.2 diff --git a/web/public/Dropbox.png b/web/public/Dropbox.png deleted file mode 100644 index cd83e09eb6622919b416e635d611c43893a07a4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43377 zcmYhi1yq#Z^EkW$(%s!D-6dU1mvl*YmvkdYEg>BuEh*h7(%oIsjlk0NKKl86|K~ji z*Sqt~ow_q~XJ&V!)l}rrkcp8&AP|~7SXu)FLd*pIfe>Kfr7XGZw&xJFaNg&9tCZKJof)R-30~gfr1bJJ=z8Z zZ-WAXzyG!ar2eggPe8$&pnx4v@IUzfd-~tP^&bm>zW?T5?f?*=ruB~(uss-9{7)VL z$Lr`X9pKOJf3gK1{2#mE^?!T+A^(R1kODLcp8coPKY~7ce{uW)G5#$8%KrZ&{^9pO zIsnK2jsL@|PO0_)qx%nEtm0UjOO{Z2!*={|xw7cmVl7ss7(C zz%qLvpKZ|pqyK*k0PR1H0E=Ca<1#4d-&_Cxor2&04CoIS|9@ft3*dBtjliJ*0{kb8 zFJPAcjQ@`UV0l31|BeHI|39?9>wk3o{-FWuzh?xH`bXz4{(ox#{!g#JhTZ#1=kKn6 z`u@ibcnd55N&dd>|HJvW{_p93uYYL*1ph}9cneVY5BPWEKWYDB{%`&F=|53{*Z+_K z$o~)F<>kd3MKcy~-CYk26-^LE;x}ZSVSa_p3G!FL&rcaB*t@)_RjZO(hd279Bk07{ zLi;qyuxsb|g*@7^9X3D4)iTf+3<{bG>+y~1?u zjdh_&JNM?%N#^vPsPgrc@kuLY(nI@Fhqwg&0_&iy+`8cGo(3gO*T#{^tx>)5pK^r! zpLECLWZhR(JnX%UECU6cV|@feTcV39+Dh49TO4Z|E#6Zs8%nE}tZ{tF(&UqJ5EE$7 zZJRXbu2n0>XO_(zO=-5zn>H~hE3!+j0RJe=jgK@nO9+ez@$d>tbw#ofPXjc5Wu_=6 zz1L`Omkhi?@Q_!QLD)mWLq_3?&5G*=q8%hJE%ENd((zMt*t^*rfj@j`t|v~&m{>@m zp)#0hY;6)SUxS-5dtNhPS{63E%J7nUYQa>a4JDK5c^&s@nJ_yY%p;&K7uQ3`6OWTT z5}WDcynAvsyMDe&3@IvaJ6!WS?Z_J!n=sn*@7l|4UDsTno&ZH3%r685s|Sbs?|lhF zgZXiaE?BQ2cDQ*boLd?2xdID#OE>d9I{)S#K`!SSK z1`j__K8^A1c$jC$DXN#q`jq>12>}`!M3FnwTKl@PXmMX0I+odxhg*?dIfx07+RHXr z?7Flfp^RF>CBpySJNhFt(R zNyJ-JLYi&j#H%E3%)}z#B4?R5j4;MS|$O9jL*S(q5eZ+`F{_5JB^ew5T zV0(rD0;^-JRi>Rk?IHp9iENhsw}!V9jNW{`gHMsKPBn*_nzQ4}LJaM(9qId?U>4!6 zOoeL0BRLo~>#*u0BmA+pwe(kuPqmHJolu0vNBCdxSM?WtYgVR;rlqE^wFkvn%pLPr zL8zoN`zOn(3qcA$zNFvuzG9T)n2oiBQ~@i%q?BG$WIc& z5PknTp;0#3N!kh>m zhVW=JOxd)au}<`ln?4f#{Wv1AXlk4`h1E!Wa9V#}sr1%hB|O15{`;~hjq{N(G&TtJ zmi?h((0f%eC;$9IcBGH8sh?m`!eZGW_~UO4q9^M2VmaRJK1oevc0VMK36CI|P-b!;P}K}pAX(`uWF7*I+%wq)7>bl}4w-Ogoq` zCs2*sJZ*Xc=5dzplk^nVk9B+q0M&xP%^kA#(lrTpXy=)1t)iozHD%uyh* z?Z&HV*1Fw*r(va?#BJZx8FmpnM-F6VdBJ5Tk$h6NLX4UN@<8bIwzj+Un46o_f}qbC zwL6@&jYK|nYl#W3^0xfLj|q1-cfENkO0EznU}D=NW!#p@Cw8WR!66!h5nRk@=IU>S z-J7*7)C}}1S@DIWlPk8h!L9cCjqOSJU5%(oC?)F|nT2nUd8S{_EE3JX)7XZo%pC}s zr!Qt?0vy>2wX~q|nx`@(sLseStHS;bhLVnU?p>=q?BOnu25jrL(WYgKM{ng1Yu(

19rC;Uy7sCh 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≀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 { - 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, - error: isCredentialsError, - refreshCredentials, - } = usePublicCredentials(); - - if ( - (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) || - (!credentialsData && isCredentialsLoading) - ) { - return ; - } - - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return

Failed to load connectors
; - } - - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; - } - - const dropboxConnectorIndexingStatuses: ConnectorIndexingStatus< - DropboxConfig, - DropboxCredentialJson - >[] = connectorIndexingStatuses.filter( - (connectorIndexingStatus) => - connectorIndexingStatus.connector.source === "dropbox" - ); - const dropboxCredential: Credential | undefined = - credentialsData.find( - (credential) => credential.credential_json?.dropbox_access_token - ); - - return ( - <> - {popup} - - Provide your API details - - - {dropboxCredential ? ( - <> -
-

Existing API Token:

-

- {dropboxCredential.credential_json?.dropbox_access_token} -

- -
- - ) : ( - <> - - See the Dropbox connector{" "} -
- setup guide - {" "} - on the Danswer docs to obtain a Dropbox token. - - - - formBody={ - <> - - - } - validationSchema={Yup.object().shape({ - dropbox_access_token: Yup.string().required( - "Please enter your Dropbox API token" - ), - })} - initialValues={{ - dropbox_access_token: "", - }} - onSubmit={(isSuccess) => { - if (isSuccess) { - refreshCredentials(); - mutate("/api/manage/admin/connector/indexing-status"); - } - }} - /> - - - )} - - {dropboxConnectorIndexingStatuses.length > 0 && ( - <> - - Dropbox indexing status - - - Due to the short term access key, the Dropbox connector will only - index files after a new access key is provided and the indexing - process is re-run manually. - -
- - connectorIndexingStatuses={dropboxConnectorIndexingStatuses} - liveCredential={dropboxCredential} - onCredentialLink={async (connectorId) => { - if (dropboxCredential) { - await linkCredential(connectorId, dropboxCredential.id); - mutate("/api/manage/admin/connector/indexing-status"); - } - }} - onUpdate={() => - mutate("/api/manage/admin/connector/indexing-status") - } - /> -
- - )} - - {dropboxCredential && dropboxConnectorIndexingStatuses.length === 0 && ( - <> - -

Create Connection

-

- Press connect below to start the connection to your Dropbox - instance. -

- - nameBuilder={(values) => `Dropbox`} - ccPairNameBuilder={(values) => `Dropbox`} - source="dropbox" - inputType="poll" - formBody={<>} - validationSchema={Yup.object().shape({})} - initialValues={{}} - // refreshFreq={10 * 60} // Disable polling - credentialId={dropboxCredential.id} - /> -
- - )} - - ); -}; - -export default function Page() { - return ( -
-
- -
- } title="Dropbox" /> -
-
- ); -} diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 04d003a59..8cbec72c4 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -51,7 +51,6 @@ import hubSpotIcon from "../../../public/HubSpot.png"; import document360Icon from "../../../public/Document360.png"; import googleSitesIcon from "../../../public/GoogleSites.png"; import zendeskIcon from "../../../public/Zendesk.svg"; -import dropboxIcon from "../../../public/Dropbox.png"; import sharepointIcon from "../../../public/Sharepoint.png"; import teamsIcon from "../../../public/Teams.png"; import mediawikiIcon from "../../../public/MediaWiki.svg"; @@ -618,18 +617,6 @@ export const ZendeskIcon = ({
); -export const DropboxIcon = ({ - size = 16, - className = defaultTailwindCSS, -}: IconProps) => ( -
- Logo -
-); - export const DiscourseIcon = ({ size = 16, className = defaultTailwindCSS, diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index 597f45f4d..303108394 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -4,7 +4,6 @@ import { ConfluenceIcon, DiscourseIcon, Document360Icon, - DropboxIcon, FileIcon, GithubIcon, GitlabIcon, @@ -155,11 +154,6 @@ const SOURCE_METADATA_MAP: SourceMap = { displayName: "Loopio", category: SourceCategory.AppConnection, }, - dropbox: { - icon: DropboxIcon, - displayName: "Dropbox", - category: SourceCategory.AppConnection, - }, sharepoint: { icon: SharepointIcon, displayName: "Sharepoint", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index dd37a27a5..b71a7c268 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -42,7 +42,6 @@ export type ValidSources = | "file" | "google_sites" | "loopio" - | "dropbox" | "sharepoint" | "teams" | "zendesk" @@ -196,8 +195,6 @@ export interface GoogleSitesConfig { export interface ZendeskConfig {} -export interface DropboxConfig {} - export interface MediaWikiBaseConfig { connector_name: string; language_code: string; @@ -205,7 +202,6 @@ export interface MediaWikiBaseConfig { pages?: string[]; recurse_depth?: number; } - export interface MediaWikiConfig extends MediaWikiBaseConfig { hostname: string; } @@ -226,7 +222,7 @@ export interface IndexAttemptSnapshot { export interface ConnectorIndexingStatus< ConnectorConfigType, - ConnectorCredentialType + ConnectorCredentialType, > { cc_pair_id: number; name: string | null; @@ -370,10 +366,6 @@ export interface ZendeskCredentialJson { zendesk_token: string; } -export interface DropboxCredentialJson { - dropbox_access_token: string; -} - export interface SharepointCredentialJson { sp_client_id: string; sp_client_secret: string; From 754b735174f5da9d2f611f6a73c617d47997caa6 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Fri, 7 Jun 2024 11:00:01 -0700 Subject: [PATCH 26/32] Revert "fix gitlab-connector - wrong datetime format (#1559)" This reverts commit 8dfba97c0955fe0710a57561babc85ccf9c514f1. --- .../danswer/connectors/gitlab/connector.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/backend/danswer/connectors/gitlab/connector.py b/backend/danswer/connectors/gitlab/connector.py index 55e358bbe..3b37dcdf5 100644 --- a/backend/danswer/connectors/gitlab/connector.py +++ b/backend/danswer/connectors/gitlab/connector.py @@ -6,7 +6,6 @@ from datetime import timezone from typing import Any import gitlab -import pytz from danswer.configs.app_configs import INDEX_BATCH_SIZE from danswer.configs.constants import DocumentSource @@ -115,14 +114,12 @@ class GitlabConnector(LoadConnector, PollConnector): doc_batch: list[Document] = [] for mr in mr_batch: mr.updated_at = datetime.strptime( - mr.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z" + mr.updated_at, "%Y-%m-%dT%H:%M:%S.%fZ" ) - if start is not None and mr.updated_at < start.replace( - tzinfo=pytz.UTC - ): + if start is not None and mr.updated_at < start: yield doc_batch return - if end is not None and mr.updated_at > end.replace(tzinfo=pytz.UTC): + if end is not None and mr.updated_at > end: continue doc_batch.append(_convert_merge_request_to_document(mr)) yield doc_batch @@ -134,17 +131,13 @@ class GitlabConnector(LoadConnector, PollConnector): doc_batch = [] for issue in issue_batch: issue.updated_at = datetime.strptime( - issue.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z" + issue.updated_at, "%Y-%m-%dT%H:%M:%S.%fZ" ) - if start is not None: - start = start.replace(tzinfo=pytz.UTC) - if issue.updated_at < start: - yield doc_batch - return - if end is not None: - end = end.replace(tzinfo=pytz.UTC) - if issue.updated_at > end: - continue + if start is not None and issue.updated_at < start: + yield doc_batch + return + if end is not None and issue.updated_at > end: + continue doc_batch.append(_convert_issue_to_document(issue)) yield doc_batch From b79820a309286530a9ce0c61ff4faf717aaf2f2f Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Fri, 7 Jun 2024 11:05:57 -0700 Subject: [PATCH 27/32] Revert "Update init-letsencrypt.sh (#1380)" This reverts commit 9e0b6aa5312400b46ea02ee47d7eb09c24960f07. --- deployment/docker_compose/init-letsencrypt.sh | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/deployment/docker_compose/init-letsencrypt.sh b/deployment/docker_compose/init-letsencrypt.sh index 9eec409fa..5eb3c73b9 100755 --- a/deployment/docker_compose/init-letsencrypt.sh +++ b/deployment/docker_compose/init-letsencrypt.sh @@ -21,13 +21,7 @@ docker_compose_cmd() { # Assign appropriate Docker Compose command COMPOSE_CMD=$(docker_compose_cmd) -# Only add www to domain list if domain wasn't explicitly set as a subdomain -if [[ ! $DOMAIN == www.* ]]; then - domains=("$DOMAIN" "www.$DOMAIN") -else - domains=("$DOMAIN") -fi - +domains=("$DOMAIN" "www.$DOMAIN") rsa_key_size=4096 data_path="../data/certbot" email="$EMAIL" # Adding a valid address is strongly recommended From 16e023a8cec13908fd530a96cbf3ca463e8e05a8 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Fri, 7 Jun 2024 11:20:58 -0700 Subject: [PATCH 28/32] Revert "ran prettier" This reverts commit 750c1df0bbdddeedfb036f085ead7a4a850b6ef1. --- web/src/app/admin/connectors/teams/page.tsx | 14 +++++++------- web/src/app/chat/ChatPage.tsx | 8 ++++---- web/src/app/chat/message/custom-code-styles.css | 4 +++- web/src/app/chat/useDocumentSelection.ts | 2 +- web/src/components/Bubble.tsx | 4 ++-- .../admin/connectors/table/ConnectorsTable.tsx | 4 ++-- .../connectors/table/SingleUseConnectorsTable.tsx | 2 +- web/src/lib/assistants/fetchPersonaEditorInfoSS.ts | 4 ++-- 8 files changed, 22 insertions(+), 20 deletions(-) diff --git a/web/src/app/admin/connectors/teams/page.tsx b/web/src/app/admin/connectors/teams/page.tsx index 30ea4d3bc..4ec0c9837 100644 --- a/web/src/app/admin/connectors/teams/page.tsx +++ b/web/src/app/admin/connectors/teams/page.tsx @@ -73,9 +73,9 @@ const MainSection = () => { return ( <> - The Teams connector allows you to index and search through your Teams - channels. Once setup, all messages from the channels contained in the - specified teams will be queryable within Danswer. + The Teams connector allows you to index and search through your + Teams channels. Once setup, all messages from the channels contained + in the specified teams will be queryable within Danswer. @@ -165,8 +165,8 @@ const MainSection = () => { {teamsConnectorIndexingStatuses.length > 0 && ( <> <Text className="mb-2"> - The latest messages from the specified teams are fetched every 10 - minutes. + The latest messages from the specified teams are + fetched every 10 minutes. </Text> <div className="mb-2"> <ConnectorsTable<TeamsConfig, TeamsCredentialJson> @@ -241,8 +241,8 @@ const MainSection = () => { ) : ( <Text> Please provide all Azure info in Step 1 first! Once you're done - with that, you can then specify which teams you want to make - searchable. + with that, you can then specify which teams you want to + make searchable. </Text> )} </> diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 1fec3bb27..3a385c1af 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -332,10 +332,10 @@ export function ChatPage({ (persona) => persona.id === existingChatSessionPersonaId ) : defaultSelectedPersonaId !== undefined - ? filteredAssistants.find( - (persona) => persona.id === defaultSelectedPersonaId - ) - : undefined + ? filteredAssistants.find( + (persona) => persona.id === defaultSelectedPersonaId + ) + : undefined ); const livePersona = selectedPersona || filteredAssistants[0] || availablePersonas[0]; diff --git a/web/src/app/chat/message/custom-code-styles.css b/web/src/app/chat/message/custom-code-styles.css index 30dee5540..b7d419beb 100644 --- a/web/src/app/chat/message/custom-code-styles.css +++ b/web/src/app/chat/message/custom-code-styles.css @@ -21,7 +21,9 @@ pre[class*="language-"] { ::-webkit-scrollbar-thumb { background: #4b5563; /* Dark handle color */ border-radius: 10px; - transition: background 0.3s ease, box-shadow 0.3s ease; /* Smooth transition for hover effect */ + transition: + background 0.3s ease, + box-shadow 0.3s ease; /* Smooth transition for hover effect */ } ::-webkit-scrollbar-thumb:hover { diff --git a/web/src/app/chat/useDocumentSelection.ts b/web/src/app/chat/useDocumentSelection.ts index 9e5fa3d2c..df33f13c3 100644 --- a/web/src/app/chat/useDocumentSelection.ts +++ b/web/src/app/chat/useDocumentSelection.ts @@ -21,7 +21,7 @@ export function useDocumentSelection(): [ DanswerDocument[], (document: DanswerDocument) => void, () => void, - number + number, ] { const [selectedDocuments, setSelectedDocuments] = useState<DanswerDocument[]>( [] diff --git a/web/src/components/Bubble.tsx b/web/src/components/Bubble.tsx index 8bf6f1977..1b559ec24 100644 --- a/web/src/components/Bubble.tsx +++ b/web/src/components/Bubble.tsx @@ -27,8 +27,8 @@ export function Bubble({ (notSelectable ? " bg-background cursor-default" : isSelected - ? " bg-hover cursor-pointer" - : " bg-background hover:bg-hover-light cursor-pointer") + ? " bg-hover cursor-pointer" + : " bg-background hover:bg-hover-light cursor-pointer") } onClick={onClick} > diff --git a/web/src/components/admin/connectors/table/ConnectorsTable.tsx b/web/src/components/admin/connectors/table/ConnectorsTable.tsx index 1a84fdcd7..be9448bac 100644 --- a/web/src/components/admin/connectors/table/ConnectorsTable.tsx +++ b/web/src/components/admin/connectors/table/ConnectorsTable.tsx @@ -87,7 +87,7 @@ export function StatusRow<ConnectorConfigType, ConnectorCredentialType>({ export interface ColumnSpecification< ConnectorConfigType, - ConnectorCredentialType + ConnectorCredentialType, > { header: string; key: string; @@ -101,7 +101,7 @@ export interface ColumnSpecification< export interface ConnectorsTableProps< ConnectorConfigType, - ConnectorCredentialType + ConnectorCredentialType, > { connectorIndexingStatuses: ConnectorIndexingStatus< ConnectorConfigType, diff --git a/web/src/components/admin/connectors/table/SingleUseConnectorsTable.tsx b/web/src/components/admin/connectors/table/SingleUseConnectorsTable.tsx index 47d848357..f1308a995 100644 --- a/web/src/components/admin/connectors/table/SingleUseConnectorsTable.tsx +++ b/web/src/components/admin/connectors/table/SingleUseConnectorsTable.tsx @@ -46,7 +46,7 @@ const SingleUseConnectorStatus = ({ export function SingleUseConnectorsTable< ConnectorConfigType, - ConnectorCredentialType + ConnectorCredentialType, >({ connectorIndexingStatuses, liveCredential, diff --git a/web/src/lib/assistants/fetchPersonaEditorInfoSS.ts b/web/src/lib/assistants/fetchPersonaEditorInfoSS.ts index 17484cd1b..b98564bee 100644 --- a/web/src/lib/assistants/fetchPersonaEditorInfoSS.ts +++ b/web/src/lib/assistants/fetchPersonaEditorInfoSS.ts @@ -18,7 +18,7 @@ export async function fetchAssistantEditorInfoSS( existingPersona: Persona | null; tools: ToolSnapshot[]; }, - null + null, ] | [null, string] > { @@ -50,7 +50,7 @@ export async function fetchAssistantEditorInfoSS( Response, User | null, ToolSnapshot[] | null, - Response | null + Response | null, ]; if (!ccPairsInfoResponse.ok) { From ff5985832793461d7684bf9830c39ae1db2a4601 Mon Sep 17 00:00:00 2001 From: hagen-danswer <hagen@danswer.ai> Date: Fri, 7 Jun 2024 13:19:48 -0700 Subject: [PATCH 29/32] final bugfixes --- backend/danswer/connectors/teams/connector.py | 22 ++++++++++++++----- web/src/lib/types.ts | 6 ----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index 3362c1963..e6a3f6120 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -164,8 +164,12 @@ class TeamsConnector(LoadConnector, PollConnector): def _construct_semantic_identifier( self, channel: Channel, top_message: ChatMessage - ) -> str: - first_poster = top_message.properties["from"]["user"]["displayName"] + ) -> str | None: + first_poster = ( + top_message.properties.get("from", {}) + .get("user", {}) + .get("displayName", {}) + ) channel_name = channel.properties["displayName"] thread_subject = top_message.properties["subject"] snippet = parse_html_page_basic( @@ -173,6 +177,8 @@ class TeamsConnector(LoadConnector, PollConnector): if len(top_message.body.content) > 50 else top_message.body.content ) + if not first_poster or not channel_name or not thread_subject or not snippet: + return None return f"{first_poster} in {channel_name} about {thread_subject}: {snippet}" @@ -187,7 +193,7 @@ class TeamsConnector(LoadConnector, PollConnector): most_recent_message_datetime: datetime | None = None top_message = thread[0] post_members_list: list[BasicExpertInfo] = [] - messages_text = "" + thread_text = "" sorted_thread = sorted(thread, key=get_created_datetime, reverse=True) @@ -202,7 +208,7 @@ class TeamsConnector(LoadConnector, PollConnector): # add text and a newline if message.body.content: message_text = parse_html_page_basic(message.body.content) - messages_text += message_text + thread_text += message_text # if it has a subject, that means its the top level post message, so grab its id, url, and subject if message.properties["subject"]: @@ -227,13 +233,19 @@ class TeamsConnector(LoadConnector, PollConnector): if not post_members_list: post_members_list = self._extract_channel_members(channel) + if not thread_text: + return None + semantic_string = self._construct_semantic_identifier(channel, top_message) + if not semantic_string: + return None + post_id = top_message.properties["id"] web_url = top_message.web_url doc = Document( id=post_id, - sections=[Section(link=web_url, text=messages_text)], + sections=[Section(link=web_url, text=thread_text)], source=DocumentSource.TEAMS, semantic_identifier=semantic_string, doc_updated_at=most_recent_message_datetime, diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index b71a7c268..d21d5862e 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -391,12 +391,6 @@ export interface AxeroCredentialJson { export interface MediaWikiCredentialJson {} export interface WikipediaCredentialJson extends MediaWikiCredentialJson {} -export interface TeamsCredentialJson { - aad_client_id: string; - aad_client_secret: string; - aad_directory_id: string; -} - // DELETION export interface DeletionAttemptSnapshot { From cbc53fd500d49c6c552adfb9a877993decce708f Mon Sep 17 00:00:00 2001 From: hagen-danswer <hagen@danswer.ai> Date: Fri, 7 Jun 2024 20:34:24 -0700 Subject: [PATCH 30/32] moved methods to top and fixed logic errors --- backend/danswer/connectors/teams/connector.py | 299 +++++++++--------- 1 file changed, 146 insertions(+), 153 deletions(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index e6a3f6120..3fadb286b 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -34,6 +34,149 @@ def get_created_datetime(chat_message: ChatMessage) -> datetime: ) +def _extract_channel_members(channel: Channel) -> list[BasicExpertInfo]: + channel_members_list: list[BasicExpertInfo] = [] + members = channel.members.get().execute_query() + for member in members: + channel_members_list.append(BasicExpertInfo(display_name=member.display_name)) + return channel_members_list + + +def _get_threads_from_channel( + channel: Channel, + start: datetime | None = None, + end: datetime | None = None, +) -> list[list[ChatMessage]]: + # Ensure start and end are timezone-aware + if start and start.tzinfo is None: + start = start.replace(tzinfo=timezone.utc) + if end and end.tzinfo is None: + end = end.replace(tzinfo=timezone.utc) + + query = channel.messages.get() + base_messages: list[ChatMessage] = query.execute_query() + + threads: list[list[ChatMessage]] = [] + for base_message in base_messages: + message_datetime = datetime.strptime( + base_message.properties["lastModifiedDateTime"], datetime_format_string + ) + + if start and message_datetime < start: + continue + if end and message_datetime > end: + continue + + reply_query = base_message.replies.get_all() + replies = reply_query.execute_query() + + # start a list containing the base message and its replies + thread: list[ChatMessage] = [base_message] + thread.extend(replies) + + threads.append(thread) + + return threads + + +def _get_channels_from_teams( + teams: list[Team], +) -> list[Channel]: + channels_list: list[Channel] = [] + for team in teams: + query = team.channels.get() + channels = query.execute_query() + channels_list.extend(channels) + + return channels_list + + +def _construct_semantic_identifier(channel: Channel, top_message: ChatMessage) -> str: + first_poster = ( + top_message.properties.get("from", {}) + .get("user", {}) + .get("displayName", "Unknown User") + ) + channel_name = channel.properties.get("displayName", "Unknown") + thread_subject = top_message.properties.get("subject", "Unknown") + + snippet = parse_html_page_basic(top_message.body.content.rstrip()) + snippet = snippet[:50] + "..." if len(snippet) > 50 else snippet + + return f"{first_poster} in {channel_name} about {thread_subject}: {snippet}" + + +def _convert_thread_to_document( + channel: Channel, + thread: list[ChatMessage], +) -> Document | None: + if len(thread) == 0: + return None + + most_recent_message_datetime: datetime | None = None + top_message = thread[0] + post_members_list: list[BasicExpertInfo] = [] + thread_text = "" + + sorted_thread = sorted(thread, key=get_created_datetime, reverse=True) + + if sorted_thread: + most_recent_message = sorted_thread[0] + most_recent_message_datetime = datetime.strptime( + most_recent_message.properties["createdDateTime"], + datetime_format_string, + ) + + for message in thread: + # add text and a newline + if message.body.content: + message_text = parse_html_page_basic(message.body.content) + thread_text += message_text + + # if it has a subject, that means its the top level post message, so grab its id, url, and subject + if message.properties["subject"]: + top_message = message + + # check to make sure there is a valid display name + if message.properties["from"]: + if message.properties["from"]["user"]: + if message.properties["from"]["user"]["displayName"]: + message_sender = message.properties["from"]["user"]["displayName"] + # if its not a duplicate, add it to the list + if message_sender not in [ + member.display_name for member in post_members_list + ]: + post_members_list.append( + BasicExpertInfo(display_name=message_sender) + ) + + # if there are no found post members, grab the members from the parent channel + if not post_members_list: + post_members_list = _extract_channel_members(channel) + + if not thread_text: + return None + + semantic_string = _construct_semantic_identifier(channel, top_message) + if not semantic_string: + return None + + post_id = top_message.properties["id"] + web_url = top_message.web_url + + doc = Document( + id=post_id, + sections=[Section(link=web_url, text=thread_text)], + source=DocumentSource.TEAMS, + semantic_identifier=semantic_string, + title="", # teams threads don't really have a "title" + doc_updated_at=most_recent_message_datetime, + primary_owners=post_members_list, + metadata={}, + ) + return doc + + class TeamsConnector(LoadConnector, PollConnector): def __init__( self, @@ -67,55 +210,6 @@ class TeamsConnector(LoadConnector, PollConnector): self.graph_client = GraphClient(_acquire_token_func) return None - def _get_threads_from_channel( - self, - channel: Channel, - start: datetime | None = None, - end: datetime | None = None, - ) -> list[list[ChatMessage]]: - # Ensure start and end are timezone-aware - if start and start.tzinfo is None: - start = start.replace(tzinfo=timezone.utc) - if end and end.tzinfo is None: - end = end.replace(tzinfo=timezone.utc) - - query = channel.messages.get() - base_messages: list[ChatMessage] = query.execute_query() - - threads: list[list[ChatMessage]] = [] - for base_message in base_messages: - message_datetime = datetime.strptime( - base_message.properties["lastModifiedDateTime"], datetime_format_string - ) - - if start and message_datetime < start: - continue - if end and message_datetime > end: - continue - - reply_query = base_message.replies.get_all() - replies = reply_query.execute_query() - - # start a list containing the base message and its replies - thread: list[ChatMessage] = [base_message] - thread.extend(replies) - - threads.append(thread) - - return threads - - def _get_channels_from_teams( - self, - teams: list[Team], - ) -> list[Channel]: - channels_list: list[Channel] = [] - for team in teams: - query = team.channels.get() - channels = query.execute_query() - channels_list.extend(channels) - - return channels_list - def _get_all_teams(self) -> list[Team]: if self.graph_client is None: raise ConnectorMissingCredentialError("Teams") @@ -144,16 +238,16 @@ class TeamsConnector(LoadConnector, PollConnector): teams = self._get_all_teams() - channels = self._get_channels_from_teams( + channels = _get_channels_from_teams( teams=teams, ) # goes over channels, converts them into Document objects and then yields them in batches doc_batch: list[Document] = [] for channel in channels: - thread_list = self._get_threads_from_channel(channel, start=start, end=end) + thread_list = _get_threads_from_channel(channel, start=start, end=end) for thread in thread_list: - converted_doc = self._convert_thread_to_document(channel, thread) + converted_doc = _convert_thread_to_document(channel, thread) if converted_doc: doc_batch.append(converted_doc) @@ -162,107 +256,6 @@ class TeamsConnector(LoadConnector, PollConnector): doc_batch = [] yield doc_batch - def _construct_semantic_identifier( - self, channel: Channel, top_message: ChatMessage - ) -> str | None: - first_poster = ( - top_message.properties.get("from", {}) - .get("user", {}) - .get("displayName", {}) - ) - channel_name = channel.properties["displayName"] - thread_subject = top_message.properties["subject"] - snippet = parse_html_page_basic( - top_message.body.content[:50].rstrip() + "..." - if len(top_message.body.content) > 50 - else top_message.body.content - ) - if not first_poster or not channel_name or not thread_subject or not snippet: - return None - - return f"{first_poster} in {channel_name} about {thread_subject}: {snippet}" - - def _convert_thread_to_document( - self, - channel: Channel, - thread: list[ChatMessage], - ) -> Document | None: - if len(thread) <= 0: - return None - - most_recent_message_datetime: datetime | None = None - top_message = thread[0] - post_members_list: list[BasicExpertInfo] = [] - thread_text = "" - - sorted_thread = sorted(thread, key=get_created_datetime, reverse=True) - - if sorted_thread: - most_recent_message = sorted_thread[0] - most_recent_message_datetime = datetime.strptime( - most_recent_message.properties["createdDateTime"], - datetime_format_string, - ) - - for message in thread: - # add text and a newline - if message.body.content: - message_text = parse_html_page_basic(message.body.content) - thread_text += message_text - - # if it has a subject, that means its the top level post message, so grab its id, url, and subject - if message.properties["subject"]: - top_message = message - - # check to make sure there is a valid display name - if message.properties["from"]: - if message.properties["from"]["user"]: - if message.properties["from"]["user"]["displayName"]: - message_sender = message.properties["from"]["user"][ - "displayName" - ] - # if its not a duplicate, add it to the list - if message_sender not in [ - member.display_name for member in post_members_list - ]: - post_members_list.append( - BasicExpertInfo(display_name=message_sender) - ) - - # if there are no found post members, grab the members from the parent channel - if not post_members_list: - post_members_list = self._extract_channel_members(channel) - - if not thread_text: - return None - - semantic_string = self._construct_semantic_identifier(channel, top_message) - if not semantic_string: - return None - - post_id = top_message.properties["id"] - web_url = top_message.web_url - - doc = Document( - id=post_id, - sections=[Section(link=web_url, text=thread_text)], - source=DocumentSource.TEAMS, - semantic_identifier=semantic_string, - doc_updated_at=most_recent_message_datetime, - primary_owners=post_members_list, - metadata={}, - ) - return doc - - def _extract_channel_members(self, channel: Channel) -> list[BasicExpertInfo]: - channel_members_list: list[BasicExpertInfo] = [] - members = channel.members.get().execute_query() - for member in members: - channel_members_list.append( - BasicExpertInfo(display_name=member.display_name) - ) - return channel_members_list - def load_from_state(self) -> GenerateDocumentsOutput: return self._fetch_from_teams() From be5dd3eefb015d5229c4559c8f12819abfd78b97 Mon Sep 17 00:00:00 2001 From: hagen-danswer <hagen@danswer.ai> Date: Mon, 10 Jun 2024 09:32:39 -0700 Subject: [PATCH 31/32] final revisions fr --- backend/danswer/connectors/teams/connector.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/backend/danswer/connectors/teams/connector.py b/backend/danswer/connectors/teams/connector.py index 3fadb286b..3b9340878 100644 --- a/backend/danswer/connectors/teams/connector.py +++ b/backend/danswer/connectors/teams/connector.py @@ -11,6 +11,7 @@ from office365.teams.team import Team # type: ignore from danswer.configs.app_configs import INDEX_BATCH_SIZE from danswer.configs.constants import DocumentSource +from danswer.connectors.cross_connector_utils.miscellaneous_utils import time_str_to_utc from danswer.connectors.interfaces import GenerateDocumentsOutput from danswer.connectors.interfaces import LoadConnector from danswer.connectors.interfaces import PollConnector @@ -24,14 +25,10 @@ from danswer.utils.logger import setup_logger logger = setup_logger() -datetime_format_string = "%Y-%m-%dT%H:%M:%S.%f%z" - def get_created_datetime(chat_message: ChatMessage) -> datetime: # Extract the 'createdDateTime' value from the 'properties' dictionary and convert it to a datetime object - return datetime.strptime( - chat_message.properties["createdDateTime"], datetime_format_string - ) + return time_str_to_utc(chat_message.properties["createdDateTime"]) def _extract_channel_members(channel: Channel) -> list[BasicExpertInfo]: @@ -58,8 +55,8 @@ def _get_threads_from_channel( threads: list[list[ChatMessage]] = [] for base_message in base_messages: - message_datetime = datetime.strptime( - base_message.properties["lastModifiedDateTime"], datetime_format_string + message_datetime = time_str_to_utc( + base_message.properties["lastModifiedDateTime"] ) if start and message_datetime < start: @@ -122,9 +119,8 @@ def _convert_thread_to_document( if sorted_thread: most_recent_message = sorted_thread[0] - most_recent_message_datetime = datetime.strptime( - most_recent_message.properties["createdDateTime"], - datetime_format_string, + most_recent_message_datetime = time_str_to_utc( + most_recent_message.properties["createdDateTime"] ) for message in thread: @@ -158,8 +154,6 @@ def _convert_thread_to_document( return None semantic_string = _construct_semantic_identifier(channel, top_message) - if not semantic_string: - return None post_id = top_message.properties["id"] web_url = top_message.web_url @@ -219,12 +213,15 @@ class TeamsConnector(LoadConnector, PollConnector): teams = self.graph_client.teams.get().execute_query() if len(self.requested_team_list) > 0: - for requested_team in self.requested_team_list: - adjusted_request_string = requested_team.replace(" ", "") - for team in teams: - adjusted_team_string = team.display_name.replace(" ", "") - if adjusted_team_string == adjusted_request_string: - teams_list.append(team) + adjusted_request_strings = [ + requested_team.replace(" ", "") + for requested_team in self.requested_team_list + ] + teams_list = [ + team + for team in teams + if team.display_name.replace(" ", "") in adjusted_request_strings + ] else: teams_list.extend(teams) From 73575f22d8eab97abd43a74f9186b25b9ca03ca7 Mon Sep 17 00:00:00 2001 From: hagen-danswer <hagen@danswer.ai> Date: Mon, 10 Jun 2024 09:44:20 -0700 Subject: [PATCH 32/32] prettier --- web/src/app/admin/connectors/teams/page.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/app/admin/connectors/teams/page.tsx b/web/src/app/admin/connectors/teams/page.tsx index 4ec0c9837..30ea4d3bc 100644 --- a/web/src/app/admin/connectors/teams/page.tsx +++ b/web/src/app/admin/connectors/teams/page.tsx @@ -73,9 +73,9 @@ const MainSection = () => { return ( <> <Text> - The Teams connector allows you to index and search through your - Teams channels. Once setup, all messages from the channels contained - in the specified teams will be queryable within Danswer. + The Teams connector allows you to index and search through your Teams + channels. Once setup, all messages from the channels contained in the + specified teams will be queryable within Danswer. </Text> <Title className="mb-2 mt-6 ml-auto mr-auto"> @@ -165,8 +165,8 @@ const MainSection = () => { {teamsConnectorIndexingStatuses.length > 0 && ( <> <Text className="mb-2"> - The latest messages from the specified teams are - fetched every 10 minutes. + The latest messages from the specified teams are fetched every 10 + minutes. </Text> <div className="mb-2"> <ConnectorsTable<TeamsConfig, TeamsCredentialJson> @@ -241,8 +241,8 @@ const MainSection = () => { ) : ( <Text> Please provide all Azure info in Step 1 first! Once you're done - with that, you can then specify which teams you want to - make searchable. + with that, you can then specify which teams you want to make + searchable. </Text> )} </>