Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Freshdesk #2698

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/danswer/configs/app_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
os.environ.get("POSTGRES_PASSWORD") or "password"
)
POSTGRES_HOST = os.environ.get("POSTGRES_HOST") or "localhost"
POSTGRES_PORT = os.environ.get("POSTGRES_PORT") or "5432"
POSTGRES_PORT = os.environ.get("POSTGRES_PORT") or "5433"
POSTGRES_DB = os.environ.get("POSTGRES_DB") or "postgres"

# defaults to False
Expand Down
1 change: 1 addition & 0 deletions backend/danswer/configs/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
class DocumentSource(str, Enum):
# Special case, document passed in via Danswer APIs without specifying a source type
INGESTION_API = "ingestion_api"
FRESHDESK = "freshdesk"
SLACK = "slack"
WEB = "web"
GOOGLE_DRIVE = "google_drive"
Expand Down
2 changes: 2 additions & 0 deletions backend/danswer/connectors/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from danswer.connectors.document360.connector import Document360Connector
from danswer.connectors.dropbox.connector import DropboxConnector
from danswer.connectors.file.connector import LocalFileConnector
from danswer.connectors.freshdesk.connector import FreshdeskConnector
from danswer.connectors.github.connector import GithubConnector
from danswer.connectors.gitlab.connector import GitlabConnector
from danswer.connectors.gmail.connector import GmailConnector
Expand Down Expand Up @@ -58,6 +59,7 @@ def identify_connector_class(
input_type: InputType | None = None,
) -> Type[BaseConnector]:
connector_map = {
DocumentSource.FRESHDESK: FreshdeskConnector,
DocumentSource.WEB: WebConnector,
DocumentSource.FILE: LocalFileConnector,
DocumentSource.SLACK: {
Expand Down
160 changes: 160 additions & 0 deletions backend/danswer/connectors/freshdesk/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import json
from datetime import datetime
from typing import Any
from typing import List
from typing import Optional

import requests
from bs4 import BeautifulSoup # Add this import for HTML parsing

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 FreshdeskConnector(PollConnector):
def __init__(
self,
api_key: str | None = None,
domain: str | None = None,
password: str | None = None,
batch_size: int = INDEX_BATCH_SIZE,
) -> None:
self.api_key = api_key
self.domain = domain
self.password = password
self.batch_size = batch_size

def ticket_link(self, tid: int) -> str:
return f"https://{self.domain}.freshdesk.com/helpdesk/tickets/{tid}"

def build_doc_sections_from_ticket(self, ticket: dict) -> List[Section]:
# Use list comprehension for building sections
return [
Section(
link=self.ticket_link(int(ticket["id"])),
text=json.dumps(
{
key: value
for key, value in ticket.items()
if isinstance(value, str)
},
default=str,
),
)
]

def strip_html_tags(self, html: str) -> str:
soup = BeautifulSoup(html, "html.parser")
return soup.get_text()

def load_credentials(self, credentials: dict[str, Any]) -> Optional[dict[str, Any]]:
logger.info("Loading credentials")
self.api_key = credentials.get("freshdesk_api_key")
self.domain = credentials.get("freshdesk_domain")
self.password = credentials.get("freshdesk_password")
return None

def _process_tickets(
self, start: SecondsSinceUnixEpoch | None, end: SecondsSinceUnixEpoch | None
) -> GenerateDocumentsOutput:
assert self.api_key is not None
assert self.domain is not None
assert self.password is not None

logger.info("Processing tickets")
if any([self.api_key, self.domain, self.password]) is None:
raise ConnectorMissingCredentialError("freshdesk")

freshdesk_url = (
f"https://{self.domain}.freshdesk.com/api/v2/tickets?include=description"
)
response = requests.get(freshdesk_url, auth=(self.api_key, self.password))
response.raise_for_status() # raises exception when not a 2xx response

if response.status_code != 204:
tickets = json.loads(response.content)
logger.info(f"Fetched {len(tickets)} tickets from Freshdesk API")
doc_batch: List[Document] = []

for ticket in tickets:
# Convert the "created_at", "updated_at", and "due_by" values to ISO 8601 strings
for date_field in ["created_at", "updated_at", "due_by"]:
ticket[date_field] = datetime.fromisoformat(
ticket[date_field]
).strftime("%Y-%m-%d %H:%M:%S")

# Convert all other values to strings
ticket = {
key: str(value) if not isinstance(value, str) else value
for key, value in ticket.items()
}

# Checking for overdue tickets
today = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
ticket["overdue"] = "true" if today > ticket["due_by"] else "false"

# Mapping the status field values
status_mapping = {2: "open", 3: "pending", 4: "resolved", 5: "closed"}
ticket["status"] = status_mapping.get(
ticket["status"], str(ticket["status"])
)

# Stripping HTML tags from the description field
ticket["description"] = self.strip_html_tags(ticket["description"])

# Remove extra white spaces from the description field
ticket["description"] = " ".join(ticket["description"].split())

# Use list comprehension for building sections
sections = self.build_doc_sections_from_ticket(ticket)

created_at = datetime.fromisoformat(ticket["created_at"])

doc = Document(
id=ticket["id"],
sections=sections,
source=DocumentSource.FRESHDESK,
semantic_identifier=ticket["subject"],
metadata={
key: value
for key, value in ticket.items()
if isinstance(value, str)
and key not in ["description", "description_text"]
},
doc_updated_at=created_at,
)

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 | None, end: SecondsSinceUnixEpoch | None
) -> GenerateDocumentsOutput:
yield from self._process_tickets(start, end)


if __name__ == "__main__":
import os

connector = FreshdeskConnector(
api_key=os.environ.get("FRESHDESK_API_KEY"),
domain=os.environ.get("FRESHDESK_DOMAIN"),
password=os.environ.get("FRESHDESK_PASSWORD"),
)
for doc in connector.poll_source(start=None, end=None):
print(doc)
2 changes: 1 addition & 1 deletion deployment/docker_compose/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ services:
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
ports:
- "5432:5432"
- "5433:5432"
volumes:
- db_volume:/var/lib/postgresql/data

Expand Down
2 changes: 1 addition & 1 deletion deployment/docker_compose/docker-compose.gpu-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ services:
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
ports:
- "5432:5432"
- "5433:5432"
volumes:
- db_volume:/var/lib/postgresql/data

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ services:
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
ports:
- "5432"
- "5433"
volumes:
- db_volume:/var/lib/postgresql/data

Expand Down
Binary file added web/public/Freshdesk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions web/src/components/icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import litellmIcon from "../../../public/LiteLLM.jpg";

import awsWEBP from "../../../public/Amazon.webp";
import azureIcon from "../../../public/Azure.png";
import freshdeskIcon from "../../../public/Freshdesk.png";
import asanaIcon from "../../../public/Asana.png";
import anthropicSVG from "../../../public/Anthropic.svg";
import nomicSVG from "../../../public/nomic.svg";
Expand Down Expand Up @@ -1260,6 +1261,20 @@ export const AWSIcon = ({
);
};

export const FreshdeskIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<div
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
>
<Image src={freshdeskIcon} alt="Logo" width="96" height="96" />
</div>
);
};

export const AzureIcon = ({
size = 16,
className = defaultTailwindCSS,
Expand Down
4 changes: 4 additions & 0 deletions web/src/lib/connectors/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,10 @@ For example, specifying .*-support.* as a "channel" will cause the connector to
},
],
},
freshdesk: {
description: "Configure Freshdesk connector",
values: [],
},
clickup: {
description: "Configure ClickUp connector",
values: [
Expand Down
16 changes: 16 additions & 0 deletions web/src/lib/connectors/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export interface GithubCredentialJson {
github_access_token: string;
}

export interface FreshdeskCredentialJson {
freshdesk_api_key: string;
freshdesk_password: string;
freshdesk_domain: string;
}

export interface GitlabCredentialJson {
gitlab_url: string;
gitlab_access_token: string;
Expand Down Expand Up @@ -248,6 +254,11 @@ export const credentialTemplates: Record<ValidSources, any> = {
asana: {
asana_api_token_secret: "",
} as AsanaCredentialJson,
freshdesk: {
freshdesk_api_key: "",
freshdesk_password: "",
freshdesk_domain: "",
} as FreshdeskCredentialJson,
teams: {
teams_client_id: "",
teams_client_secret: "",
Expand Down Expand Up @@ -353,6 +364,11 @@ export const credentialDisplayNames: Record<string, string> = {
// Zulip
zuliprc_content: "Zuliprc Content",

// Freshdesk
freshdesk_api_key: "Freshdesk API Key",
freshdesk_password: "Freshdesk Password",
freshdesk_domain: "Freshdesk Domain",

// Guru
guru_user: "Guru User",
guru_user_token: "Guru User Token",
Expand Down
7 changes: 7 additions & 0 deletions web/src/lib/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
GoogleStorageIcon,
ColorSlackIcon,
XenforoIcon,
FreshdeskIcon,
} from "@/components/icons/icons";
import { ValidSources } from "./types";
import {
Expand All @@ -59,6 +60,12 @@ type SourceMap = {
};

const SOURCE_METADATA_MAP: SourceMap = {
freshdesk: {
icon: FreshdeskIcon,
displayName: "Freshdesk",
category: SourceCategory.CustomerSupport,
docs: "https://docs.danswer.dev/connectors/freshdesk",
},
web: {
icon: GlobeIcon,
displayName: "Web",
Expand Down
1 change: 1 addition & 0 deletions web/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ const validSources = [
"oci_storage",
"not_applicable",
"ingestion_api",
"freshdesk",
] as const;

export type ValidSources = (typeof validSources)[number];
Expand Down
Loading