validated

This commit is contained in:
pablodanswer 2024-10-05 16:32:48 -07:00
parent 28e65669b4
commit 5c521a7916
13 changed files with 191 additions and 4 deletions

View File

@ -135,7 +135,7 @@ POSTGRES_PASSWORD = urllib.parse.quote_plus(
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

View File

@ -74,6 +74,7 @@ CELERY_PRIMARY_WORKER_LOCK_TIMEOUT = 120
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"

View File

@ -15,6 +15,7 @@ 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.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
@ -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: {

View File

@ -0,0 +1,141 @@
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.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,
domain: str,
password: str,
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: datetime, end: datetime
) -> GenerateDocumentsOutput:
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"])
today = datetime.now()
if (today - created_at).days / 30.4375 <= 2:
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_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: datetime, end: datetime) -> GenerateDocumentsOutput:
yield from self._process_tickets(start, end)

View File

@ -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

View File

@ -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

View File

@ -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

BIN
web/public/Freshdesk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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";
@ -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,

View File

@ -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: [

View File

@ -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;
@ -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: "",
@ -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",

View File

@ -38,6 +38,7 @@ import {
GoogleStorageIcon,
ColorSlackIcon,
XenforoIcon,
FreshdeskIcon,
} from "@/components/icons/icons";
import { ValidSources } from "./types";
import {
@ -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",

View File

@ -262,6 +262,7 @@ const validSources = [
"oci_storage",
"not_applicable",
"ingestion_api",
"freshdesk",
] as const;
export type ValidSources = (typeof validSources)[number];