mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-01 00:18:18 +02:00
validated
This commit is contained in:
parent
28e65669b4
commit
5c521a7916
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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: {
|
||||
|
141
backend/danswer/connectors/freshdesk/connector.py
Normal file
141
backend/danswer/connectors/freshdesk/connector.py
Normal 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)
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
BIN
web/public/Freshdesk.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -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,
|
||||
|
@ -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: [
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -262,6 +262,7 @@ const validSources = [
|
||||
"oci_storage",
|
||||
"not_applicable",
|
||||
"ingestion_api",
|
||||
"freshdesk",
|
||||
] as const;
|
||||
|
||||
export type ValidSources = (typeof validSources)[number];
|
||||
|
Loading…
x
Reference in New Issue
Block a user