From 088551a4ef63d6d65b0e75cc2a9663906e9a92a5 Mon Sep 17 00:00:00 2001 From: pablodanswer Date: Sat, 26 Oct 2024 14:58:42 -0700 Subject: [PATCH] remove rt + home-grown sitemap parsing (#2933) * remove rt * nit * add minor alembic revision * functional migration * replace usp * k * typing --- .../versions/949b4a92a401_remove_rt.py | 74 +++++ backend/danswer/connectors/factory.py | 2 - .../connectors/requesttracker/connector.py | 277 ++++++++---------- backend/danswer/utils/sitemap.py | 87 ++++-- backend/requirements/default.txt | 2 - web/public/RequestTracker.png | Bin 17302 -> 0 bytes web/src/components/icons/icons.tsx | 8 - web/src/lib/connectors/connectors.tsx | 7 - web/src/lib/connectors/credentials.ts | 17 -- web/src/lib/sources.ts | 7 - web/src/lib/types.ts | 1 - 11 files changed, 261 insertions(+), 221 deletions(-) create mode 100644 backend/alembic/versions/949b4a92a401_remove_rt.py delete mode 100644 web/public/RequestTracker.png diff --git a/backend/alembic/versions/949b4a92a401_remove_rt.py b/backend/alembic/versions/949b4a92a401_remove_rt.py new file mode 100644 index 0000000000..6dbb7859cd --- /dev/null +++ b/backend/alembic/versions/949b4a92a401_remove_rt.py @@ -0,0 +1,74 @@ +"""remove rt + +Revision ID: 949b4a92a401 +Revises: 1b10e1fda030 +Create Date: 2024-10-26 13:06:06.937969 + +""" +from alembic import op +from sqlalchemy.orm import Session + +# Import your models and constants +from danswer.db.models import ( + Connector, + ConnectorCredentialPair, + Credential, + IndexAttempt, +) +from danswer.configs.constants import DocumentSource + + +# revision identifiers, used by Alembic. +revision = "949b4a92a401" +down_revision = "1b10e1fda030" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Deletes all RequestTracker connectors and associated data + bind = op.get_bind() + session = Session(bind=bind) + + connectors_to_delete = ( + session.query(Connector) + .filter(Connector.source == DocumentSource.REQUESTTRACKER) + .all() + ) + + connector_ids = [connector.id for connector in connectors_to_delete] + + if connector_ids: + cc_pairs_to_delete = ( + session.query(ConnectorCredentialPair) + .filter(ConnectorCredentialPair.connector_id.in_(connector_ids)) + .all() + ) + + cc_pair_ids = [cc_pair.id for cc_pair in cc_pairs_to_delete] + + if cc_pair_ids: + session.query(IndexAttempt).filter( + IndexAttempt.connector_credential_pair_id.in_(cc_pair_ids) + ).delete(synchronize_session=False) + + session.query(ConnectorCredentialPair).filter( + ConnectorCredentialPair.id.in_(cc_pair_ids) + ).delete(synchronize_session=False) + + credential_ids = [cc_pair.credential_id for cc_pair in cc_pairs_to_delete] + if credential_ids: + session.query(Credential).filter(Credential.id.in_(credential_ids)).delete( + synchronize_session=False + ) + + session.query(Connector).filter(Connector.id.in_(connector_ids)).delete( + synchronize_session=False + ) + + session.commit() + + +def downgrade() -> None: + # No-op downgrade as we cannot restore deleted data + pass diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index 05f201a45c..ce74997333 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -34,7 +34,6 @@ from danswer.connectors.mediawiki.wiki import MediaWikiConnector from danswer.connectors.models import InputType from danswer.connectors.notion.connector import NotionConnector from danswer.connectors.productboard.connector import ProductboardConnector -from danswer.connectors.requesttracker.connector import RequestTrackerConnector from danswer.connectors.salesforce.connector import SalesforceConnector from danswer.connectors.sharepoint.connector import SharepointConnector from danswer.connectors.slab.connector import SlabConnector @@ -77,7 +76,6 @@ def identify_connector_class( DocumentSource.SLAB: SlabConnector, DocumentSource.NOTION: NotionConnector, DocumentSource.ZULIP: ZulipConnector, - DocumentSource.REQUESTTRACKER: RequestTrackerConnector, DocumentSource.GURU: GuruConnector, DocumentSource.LINEAR: LinearConnector, DocumentSource.HUBSPOT: HubSpotConnector, diff --git a/backend/danswer/connectors/requesttracker/connector.py b/backend/danswer/connectors/requesttracker/connector.py index 9c4590fc2e..b520d0d7ac 100644 --- a/backend/danswer/connectors/requesttracker/connector.py +++ b/backend/danswer/connectors/requesttracker/connector.py @@ -1,153 +1,124 @@ -from datetime import datetime -from datetime import timezone -from logging import DEBUG as LOG_LVL_DEBUG -from typing import Any -from typing import List -from typing import Optional - -from rt.rest1 import ALL_QUEUES -from rt.rest1 import Rt - -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 PollConnector -from danswer.connectors.interfaces import SecondsSinceUnixEpoch -from danswer.connectors.models import ConnectorMissingCredentialError -from danswer.connectors.models import Document -from danswer.connectors.models import Section -from danswer.utils.logger import setup_logger - -logger = setup_logger() - - -class RequestTrackerError(Exception): - pass - - -class RequestTrackerConnector(PollConnector): - def __init__( - self, - batch_size: int = INDEX_BATCH_SIZE, - ) -> None: - self.batch_size = batch_size - - def txn_link(self, tid: int, txn: int) -> str: - return f"{self.rt_base_url}/Ticket/Display.html?id={tid}&txn={txn}" - - def build_doc_sections_from_txn( - self, connection: Rt, ticket_id: int - ) -> List[Section]: - Sections: List[Section] = [] - - get_history_resp = connection.get_history(ticket_id) - - if get_history_resp is None: - raise RequestTrackerError(f"Ticket {ticket_id} cannot be found") - - for tx in get_history_resp: - Sections.append( - Section( - link=self.txn_link(ticket_id, int(tx["id"])), - text="\n".join( - [ - f"{k}:\n{v}\n" if k != "Attachments" else "" - for (k, v) in tx.items() - ] - ), - ) - ) - return Sections - - def load_credentials(self, credentials: dict[str, Any]) -> Optional[dict[str, Any]]: - self.rt_username = credentials.get("requesttracker_username") - self.rt_password = credentials.get("requesttracker_password") - self.rt_base_url = credentials.get("requesttracker_base_url") - return None - - # This does not include RT file attachments yet. - def _process_tickets( - self, start: datetime, end: datetime - ) -> GenerateDocumentsOutput: - if any([self.rt_username, self.rt_password, self.rt_base_url]) is None: - raise ConnectorMissingCredentialError("requesttracker") - - Rt0 = Rt( - f"{self.rt_base_url}/REST/1.0/", - self.rt_username, - self.rt_password, - ) - - Rt0.login() - - d0 = start.strftime("%Y-%m-%d %H:%M:%S") - d1 = end.strftime("%Y-%m-%d %H:%M:%S") - - tickets = Rt0.search( - Queue=ALL_QUEUES, - raw_query=f"Updated > '{d0}' AND Updated < '{d1}'", - ) - - doc_batch: List[Document] = [] - - for ticket in tickets: - ticket_keys_to_omit = ["id", "Subject"] - tid: int = int(ticket["numerical_id"]) - ticketLink: str = f"{self.rt_base_url}/Ticket/Display.html?id={tid}" - logger.info(f"Processing ticket {tid}") - doc = Document( - id=ticket["id"], - # Will add title to the first section later in processing - sections=[Section(link=ticketLink, text="")] - + self.build_doc_sections_from_txn(Rt0, tid), - source=DocumentSource.REQUESTTRACKER, - semantic_identifier=ticket["Subject"], - metadata={ - key: value - for key, value in ticket.items() - if key not in ticket_keys_to_omit - }, - ) - - doc_batch.append(doc) - - if len(doc_batch) >= self.batch_size: - yield doc_batch - doc_batch = [] - - if doc_batch: - yield doc_batch - - def poll_source( - self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch - ) -> GenerateDocumentsOutput: - # Keep query short, only look behind 1 day at maximum - one_day_ago: float = end - (24 * 60 * 60) - _start: float = start if start > one_day_ago else one_day_ago - start_datetime = datetime.fromtimestamp(_start, tz=timezone.utc) - end_datetime = datetime.fromtimestamp(end, tz=timezone.utc) - yield from self._process_tickets(start_datetime, end_datetime) - - -if __name__ == "__main__": - import time - import os - from dotenv import load_dotenv - - load_dotenv() - logger.setLevel(LOG_LVL_DEBUG) - rt_connector = RequestTrackerConnector() - rt_connector.load_credentials( - { - "requesttracker_username": os.getenv("RT_USERNAME"), - "requesttracker_password": os.getenv("RT_PASSWORD"), - "requesttracker_base_url": os.getenv("RT_BASE_URL"), - } - ) - - current = time.time() - one_day_ago = current - (24 * 60 * 60) # 1 days - latest_docs = rt_connector.poll_source(one_day_ago, current) - - for doc in latest_docs: - print(doc) +# from datetime import datetime +# from datetime import timezone +# from logging import DEBUG as LOG_LVL_DEBUG +# from typing import Any +# from typing import List +# from typing import Optional +# from rt.rest1 import ALL_QUEUES +# from rt.rest1 import Rt +# 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 PollConnector +# from danswer.connectors.interfaces import SecondsSinceUnixEpoch +# from danswer.connectors.models import ConnectorMissingCredentialError +# from danswer.connectors.models import Document +# from danswer.connectors.models import Section +# from danswer.utils.logger import setup_logger +# logger = setup_logger() +# class RequestTrackerError(Exception): +# pass +# class RequestTrackerConnector(PollConnector): +# def __init__( +# self, +# batch_size: int = INDEX_BATCH_SIZE, +# ) -> None: +# self.batch_size = batch_size +# def txn_link(self, tid: int, txn: int) -> str: +# return f"{self.rt_base_url}/Ticket/Display.html?id={tid}&txn={txn}" +# def build_doc_sections_from_txn( +# self, connection: Rt, ticket_id: int +# ) -> List[Section]: +# Sections: List[Section] = [] +# get_history_resp = connection.get_history(ticket_id) +# if get_history_resp is None: +# raise RequestTrackerError(f"Ticket {ticket_id} cannot be found") +# for tx in get_history_resp: +# Sections.append( +# Section( +# link=self.txn_link(ticket_id, int(tx["id"])), +# text="\n".join( +# [ +# f"{k}:\n{v}\n" if k != "Attachments" else "" +# for (k, v) in tx.items() +# ] +# ), +# ) +# ) +# return Sections +# def load_credentials(self, credentials: dict[str, Any]) -> Optional[dict[str, Any]]: +# self.rt_username = credentials.get("requesttracker_username") +# self.rt_password = credentials.get("requesttracker_password") +# self.rt_base_url = credentials.get("requesttracker_base_url") +# return None +# # This does not include RT file attachments yet. +# def _process_tickets( +# self, start: datetime, end: datetime +# ) -> GenerateDocumentsOutput: +# if any([self.rt_username, self.rt_password, self.rt_base_url]) is None: +# raise ConnectorMissingCredentialError("requesttracker") +# Rt0 = Rt( +# f"{self.rt_base_url}/REST/1.0/", +# self.rt_username, +# self.rt_password, +# ) +# Rt0.login() +# d0 = start.strftime("%Y-%m-%d %H:%M:%S") +# d1 = end.strftime("%Y-%m-%d %H:%M:%S") +# tickets = Rt0.search( +# Queue=ALL_QUEUES, +# raw_query=f"Updated > '{d0}' AND Updated < '{d1}'", +# ) +# doc_batch: List[Document] = [] +# for ticket in tickets: +# ticket_keys_to_omit = ["id", "Subject"] +# tid: int = int(ticket["numerical_id"]) +# ticketLink: str = f"{self.rt_base_url}/Ticket/Display.html?id={tid}" +# logger.info(f"Processing ticket {tid}") +# doc = Document( +# id=ticket["id"], +# # Will add title to the first section later in processing +# sections=[Section(link=ticketLink, text="")] +# + self.build_doc_sections_from_txn(Rt0, tid), +# source=DocumentSource.REQUESTTRACKER, +# semantic_identifier=ticket["Subject"], +# metadata={ +# key: value +# for key, value in ticket.items() +# if key not in ticket_keys_to_omit +# }, +# ) +# doc_batch.append(doc) +# if len(doc_batch) >= self.batch_size: +# yield doc_batch +# doc_batch = [] +# if doc_batch: +# yield doc_batch +# def poll_source( +# self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch +# ) -> GenerateDocumentsOutput: +# # Keep query short, only look behind 1 day at maximum +# one_day_ago: float = end - (24 * 60 * 60) +# _start: float = start if start > one_day_ago else one_day_ago +# start_datetime = datetime.fromtimestamp(_start, tz=timezone.utc) +# end_datetime = datetime.fromtimestamp(end, tz=timezone.utc) +# yield from self._process_tickets(start_datetime, end_datetime) +# if __name__ == "__main__": +# import time +# import os +# from dotenv import load_dotenv +# load_dotenv() +# logger.setLevel(LOG_LVL_DEBUG) +# rt_connector = RequestTrackerConnector() +# rt_connector.load_credentials( +# { +# "requesttracker_username": os.getenv("RT_USERNAME"), +# "requesttracker_password": os.getenv("RT_PASSWORD"), +# "requesttracker_base_url": os.getenv("RT_BASE_URL"), +# } +# ) +# current = time.time() +# one_day_ago = current - (24 * 60 * 60) # 1 days +# latest_docs = rt_connector.poll_source(one_day_ago, current) +# for doc in latest_docs: +# print(doc) diff --git a/backend/danswer/utils/sitemap.py b/backend/danswer/utils/sitemap.py index ababbec457..551b2bb3bf 100644 --- a/backend/danswer/utils/sitemap.py +++ b/backend/danswer/utils/sitemap.py @@ -1,39 +1,78 @@ -from datetime import datetime -from urllib import robotparser +import re +import xml.etree.ElementTree as ET +from typing import Set +from urllib.parse import urljoin -from usp.tree import sitemap_tree_for_homepage # type: ignore +import requests from danswer.utils.logger import setup_logger logger = setup_logger() -def test_url(rp: robotparser.RobotFileParser | None, url: str) -> bool: - if not rp: - return True - else: - return rp.can_fetch("*", url) +def _get_sitemap_locations_from_robots(base_url: str) -> Set[str]: + """Extract sitemap URLs from robots.txt""" + sitemap_urls: set = set() + try: + robots_url = urljoin(base_url, "/robots.txt") + resp = requests.get(robots_url, timeout=10) + if resp.status_code == 200: + for line in resp.text.splitlines(): + if line.lower().startswith("sitemap:"): + sitemap_url = line.split(":", 1)[1].strip() + sitemap_urls.add(sitemap_url) + except Exception as e: + logger.warning(f"Error fetching robots.txt: {e}") + return sitemap_urls -def init_robots_txt(site: str) -> robotparser.RobotFileParser: - ts = datetime.now().timestamp() - robots_url = f"{site}/robots.txt?ts={ts}" - rp = robotparser.RobotFileParser() - rp.set_url(robots_url) - rp.read() - return rp +def _extract_urls_from_sitemap(sitemap_url: str) -> Set[str]: + """Extract URLs from a sitemap XML file""" + urls: set[str] = set() + try: + resp = requests.get(sitemap_url, timeout=10) + if resp.status_code != 200: + return urls + + root = ET.fromstring(resp.content) + + # Handle both regular sitemaps and sitemap indexes + # Remove namespace for easier parsing + namespace = re.match(r"\{.*\}", root.tag) + ns = namespace.group(0) if namespace else "" + + if root.tag == f"{ns}sitemapindex": + # This is a sitemap index + for sitemap in root.findall(f".//{ns}loc"): + if sitemap.text: + sub_urls = _extract_urls_from_sitemap(sitemap.text) + urls.update(sub_urls) + else: + # This is a regular sitemap + for url in root.findall(f".//{ns}loc"): + if url.text: + urls.add(url.text) + + except Exception as e: + logger.warning(f"Error processing sitemap {sitemap_url}: {e}") + + return urls def list_pages_for_site(site: str) -> list[str]: - rp: robotparser.RobotFileParser | None = None - try: - rp = init_robots_txt(site) - except Exception: - logger.warning("Failed to load robots.txt") + """Get list of pages from a site's sitemaps""" + site = site.rstrip("/") + all_urls = set() - tree = sitemap_tree_for_homepage(site) + # Try both common sitemap locations + sitemap_paths = ["/sitemap.xml", "/sitemap_index.xml"] + for path in sitemap_paths: + sitemap_url = urljoin(site, path) + all_urls.update(_extract_urls_from_sitemap(sitemap_url)) - pages = [page.url for page in tree.all_pages() if test_url(rp, page.url)] - pages = list(dict.fromkeys(pages)) + # Check robots.txt for additional sitemaps + sitemap_locations = _get_sitemap_locations_from_robots(site) + for sitemap_url in sitemap_locations: + all_urls.update(_extract_urls_from_sitemap(sitemap_url)) - return pages + return list(all_urls) diff --git a/backend/requirements/default.txt b/backend/requirements/default.txt index 2240180e35..fe0cdd0154 100644 --- a/backend/requirements/default.txt +++ b/backend/requirements/default.txt @@ -61,7 +61,6 @@ requests==2.32.2 requests-oauthlib==1.3.1 retry==0.9.2 # This pulls in py which is in CVE-2022-42969, must remove py from image rfc3986==1.5.0 -rt==3.1.2 simple-salesforce==1.12.6 slack-sdk==3.20.2 SQLAlchemy[mypy]==2.0.15 @@ -79,7 +78,6 @@ asana==5.0.8 zenpy==2.0.41 dropbox==11.36.2 boto3-stubs[s3]==1.34.133 -ultimate_sitemap_parser==0.5 stripe==10.12.0 urllib3==2.2.3 mistune==0.8.4 diff --git a/web/public/RequestTracker.png b/web/public/RequestTracker.png deleted file mode 100644 index 95d6680e95cbc007dbc3609eb752d3ef9fd4febd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17302 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 W=l5kWRnPkj68CiVb6Mw<&;$U_ig}0t diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 8a7179a064..b74eaffecc 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -62,7 +62,6 @@ import OCIStorageSVG from "../../../public/OCI.svg"; import googleCloudStorageIcon from "../../../public/GoogleCloudStorage.png"; import guruIcon from "../../../public/Guru.svg"; import gongIcon from "../../../public/Gong.png"; -import requestTrackerIcon from "../../../public/RequestTracker.png"; import zulipIcon from "../../../public/Zulip.png"; import linearIcon from "../../../public/Linear.png"; import hubSpotIcon from "../../../public/HubSpot.png"; @@ -1178,13 +1177,6 @@ export const GuruIcon = ({ className = defaultTailwindCSS, }: IconProps) => ; -export const RequestTrackerIcon = ({ - size = 16, - className = defaultTailwindCSS, -}: IconProps) => ( - -); - export const SalesforceIcon = ({ size = 16, className = defaultTailwindCSS, diff --git a/web/src/lib/connectors/connectors.tsx b/web/src/lib/connectors/connectors.tsx index d722fcf984..4e7df38359 100644 --- a/web/src/lib/connectors/connectors.tsx +++ b/web/src/lib/connectors/connectors.tsx @@ -552,11 +552,6 @@ For example, specifying .*-support.* as a "channel" will cause the connector to ], advanced_values: [], }, - requesttracker: { - description: "Configure HubSpot connector", - values: [], - advanced_values: [], - }, hubspot: { description: "Configure HubSpot connector", values: [], @@ -1116,8 +1111,6 @@ export interface NotionConfig { export interface HubSpotConfig {} -export interface RequestTrackerConfig {} - export interface Document360Config { workspace: string; categories?: string[]; diff --git a/web/src/lib/connectors/credentials.ts b/web/src/lib/connectors/credentials.ts index d7bcef0ada..532f8f6de7 100644 --- a/web/src/lib/connectors/credentials.ts +++ b/web/src/lib/connectors/credentials.ts @@ -106,12 +106,6 @@ export interface HubSpotCredentialJson { hubspot_access_token: string; } -export interface RequestTrackerCredentialJson { - requesttracker_username: string; - requesttracker_password: string; - requesttracker_base_url: string; -} - export interface Document360CredentialJson { portal_id: string; document360_api_token: string; @@ -224,11 +218,6 @@ export const credentialTemplates: Record = { portal_id: "", document360_api_token: "", } as Document360CredentialJson, - requesttracker: { - requesttracker_username: "", - requesttracker_password: "", - requesttracker_base_url: "", - } as RequestTrackerCredentialJson, loopio: { loopio_subdomain: "", loopio_client_id: "", @@ -371,12 +360,6 @@ export const credentialDisplayNames: Record = { // HubSpot hubspot_access_token: "HubSpot Access Token", - - // Request Tracker - requesttracker_username: "Request Tracker Username", - requesttracker_password: "Request Tracker Password", - requesttracker_base_url: "Request Tracker Base URL", - // Document360 portal_id: "Document360 Portal ID", document360_api_token: "Document360 API Token", diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index 18bc3336ae..aa05391294 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -21,7 +21,6 @@ import { LoopioIcon, NotionIcon, ProductboardIcon, - RequestTrackerIcon, R2Icon, SalesforceIcon, SharepointIcon, @@ -243,12 +242,6 @@ const SOURCE_METADATA_MAP: SourceMap = { category: SourceCategory.Wiki, docs: "https://docs.danswer.dev/connectors/mediawiki", }, - requesttracker: { - icon: RequestTrackerIcon, - displayName: "Request Tracker", - category: SourceCategory.CustomerSupport, - docs: "https://docs.danswer.dev/connectors/requesttracker", - }, clickup: { icon: ClickupIcon, displayName: "Clickup", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index d4f43a024d..46d5efd844 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -241,7 +241,6 @@ const validSources = [ "linear", "hubspot", "document360", - "requesttracker", "file", "google_sites", "loopio",